├── wetland ├── output_plugin │ ├── __init__.py │ ├── email.py │ ├── email_bot │ ├── bearychat.py │ ├── mqtt.py │ ├── log.py │ └── jsonlog.py ├── __init__.py ├── server │ ├── __init__.py │ ├── tcpServer.py │ ├── sftpServer.py │ └── sshServer.py ├── services │ ├── __init__.py │ ├── visual.txt │ ├── direct_service.py │ ├── reverse_service.py │ ├── shell_service.py │ └── exec_service.py ├── output.py └── config.py ├── requirements ├── paramiko ├── _version.py ├── compress.py ├── kex_group14.py ├── __init__.py ├── pipe.py ├── py3compat.py ├── kex_ecdh_nist.py ├── win_pageant.py ├── ber.py ├── proxy.py ├── primes.py ├── kex_group1.py ├── sftp.py ├── ssh_exception.py ├── rsakey.py ├── ed25519key.py ├── sftp_handle.py ├── buffered_pipe.py ├── common.py ├── sftp_attr.py ├── dsskey.py ├── util.py ├── message.py └── ecdsakey.py ├── .gitignore ├── main.py ├── util ├── filechange.py ├── initwetland.py ├── clearlog.py └── playlog.py ├── data ├── id_dsa └── id_rsa ├── wetland.cfg.default └── README.md /wetland/output_plugin/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /requirements: -------------------------------------------------------------------------------- 1 | paramiko 2 | requests 3 | yagmail 4 | IPy 5 | p0f 6 | paho-mqtt 7 | -------------------------------------------------------------------------------- /wetland/__init__.py: -------------------------------------------------------------------------------- 1 | import server 2 | import config 3 | import services 4 | import output 5 | -------------------------------------------------------------------------------- /wetland/server/__init__.py: -------------------------------------------------------------------------------- 1 | import tcpServer 2 | import sshServer 3 | import sftpServer 4 | -------------------------------------------------------------------------------- /paramiko/_version.py: -------------------------------------------------------------------------------- 1 | __version_info__ = (2, 2, 1) 2 | __version__ = '.'.join(map(str, __version_info__)) 3 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | 3 | log/ 4 | files/ 5 | keys/ 6 | p0f/ 7 | download/ 8 | 9 | wetland.cfg 10 | 11 | paramiko/*.pyc 12 | wetland/*.pyc 13 | wetland/*/*.pyc 14 | -------------------------------------------------------------------------------- /wetland/services/__init__.py: -------------------------------------------------------------------------------- 1 | from shell_service import shell_service 2 | from exec_service import exec_service 3 | from direct_service import direct_service 4 | from reverse_service import reverse_handler 5 | import SocketServer 6 | -------------------------------------------------------------------------------- /main.py: -------------------------------------------------------------------------------- 1 | from wetland import config 2 | from wetland.server import tcpServer 3 | 4 | import paramiko 5 | paramiko.util.log_to_file('./paramiko.log') 6 | 7 | address = config.cfg.get("wetland", "wetland_addr") 8 | port = config.cfg.getint("wetland", "wetland_port") 9 | 10 | 11 | if __name__ == '__main__': 12 | tServer = tcpServer.tcp_server((address, port), tcpServer.tcp_handler) 13 | tServer.serve_forever() 14 | -------------------------------------------------------------------------------- /util/filechange.py: -------------------------------------------------------------------------------- 1 | import os 2 | import sys 3 | import shutil 4 | 5 | if len(sys.argv) == 1: 6 | print '[-] input container\'s id' 7 | sys.exit(1) 8 | 9 | ID = sys.argv[1] 10 | if ID == '-': 11 | ID = sys.stdin.read().strip() 12 | if len(ID) != 64: 13 | print '[-] length of ID must be 64' 14 | sys.exit(1) 15 | 16 | layer_id_file = '/var/lib/docker/image/aufs/layerdb/mounts/%s/mount-id' % ID 17 | if not os.path.exists(layer_id_file): 18 | print '[-] container not exists' 19 | 20 | with open(layer_id_file) as f: 21 | layer_id = f.read() 22 | print '[+] read-write layer ID: %s' % layer_id 23 | 24 | shutil.copytree('/var/lib/docker/aufs/diff/%s' % layer_id, layer_id) 25 | print '[+] copy to ./%s' % layer_id 26 | -------------------------------------------------------------------------------- /wetland/output.py: -------------------------------------------------------------------------------- 1 | import threading 2 | from wetland.config import cfg 3 | from importlib import import_module as impt 4 | 5 | 6 | def get_plugins(): 7 | pname = [] 8 | for k, v in cfg.items('output'): 9 | if v == 'true': 10 | pname.append(k) 11 | return pname 12 | 13 | 14 | pname = get_plugins() 15 | 16 | 17 | class output(object): 18 | def __init__(self, server): 19 | self.server = server 20 | self.plugins = [impt('wetland.output_plugin.'+n).plugin(self.server) 21 | for n in pname] 22 | 23 | def o(self, *args): 24 | for p in self.plugins: 25 | thread = threading.Thread(target=p.send, args=args) 26 | thread.setDaemon(True) 27 | thread.start() 28 | -------------------------------------------------------------------------------- /wetland/output_plugin/email.py: -------------------------------------------------------------------------------- 1 | import yagmail 2 | from wetland import config 3 | 4 | # sensor name 5 | name = config.cfg.get("wetland", "name") 6 | 7 | # smtp 8 | user = config.cfg.get("email", "user") 9 | pwd = config.cfg.get("email", "pwd") 10 | host = config.cfg.get("email", "host") 11 | port = config.cfg.getint("email", "port") 12 | 13 | # receviers 14 | tos = [v for k, v in config.cfg.items("email") if k.startswith("to")] 15 | 16 | 17 | class plugin(object): 18 | def __init__(self, hacker_ip): 19 | self.hacker_ip = hacker_ip 20 | 21 | def send(self, subject, action, content): 22 | if subject != 'wetland': 23 | return False 24 | 25 | subject = "Wetland %s Honeypot" % name 26 | 27 | text = [] 28 | text.append('Sensor:\t%s' % name) 29 | text.append('Hacker:\t%s' % self.hacker_ip) 30 | text.append('Action:\t%s' % action) 31 | text.append('Content:\t%s' % content) 32 | 33 | for to in self.tos: 34 | print self.user, self.pwd, self.host, self.port 35 | client = yagmail.SMTP(user=self.user, password=self.pwd, 36 | host=self.host, port=self.port) 37 | client.send(to=to, subject=subject, contents='\n'.join(text)) 38 | -------------------------------------------------------------------------------- /wetland/output_plugin/email_bot: -------------------------------------------------------------------------------- 1 | import yagmail 2 | from wetland import config 3 | 4 | # sensor name 5 | name = config.cfg.get("wetland", "name") 6 | 7 | # smtp 8 | user = config.cfg.get("email", "user") 9 | pwd = config.cfg.get("email", "pwd") 10 | host = config.cfg.get("email", "host") 11 | port = config.cfg.getint("email", "port") 12 | 13 | # receviers 14 | tos = [v for k, v in config.cfg.items("email") if k.startswith("to")] 15 | 16 | 17 | class plugin(object): 18 | def __init__(self, hacker_ip): 19 | self.hacker_ip = hacker_ip 20 | 21 | def send(self, subject, action, content): 22 | if subject != 'wetland': 23 | return False 24 | 25 | subject = "Wetland %s Honeypot" % name 26 | 27 | text = [] 28 | text.append('Sensor:\t%s' % name) 29 | text.append('Hacker:\t%s' % self.hacker_ip) 30 | text.append('Action:\t%s' % action) 31 | text.append('Content:\t%s' % content) 32 | 33 | for to in self.tos: 34 | print self.user, self.pwd, self.host, self.port 35 | client = yagmail.SMTP(user=self.user, password=self.pwd, 36 | host=self.host, port=self.port) 37 | client.send(to=to, subject=subject, contents='\n'.join(text)) 38 | -------------------------------------------------------------------------------- /data/id_dsa: -------------------------------------------------------------------------------- 1 | -----BEGIN DSA PRIVATE KEY----- 2 | MIIDVgIBAAKCAQEAyqnRIzfhZd4WtPwEKlfdvQyOwTs4pSrsLe1dkor83rihULQu 3 | fGdEkg0i4YshcNVk724dPDnBvSwsfzC+C4z5mLNtbCfHjeBvYD9ROUEFV9IimTPN 4 | Co684I9VcIcvf4uRyOlo5xP0W76e+0tRmTgGsjMEBIWJ/c9mKWl0Y/wu0uO3ftK/ 5 | c4FwsLN6ABpVwvKCjfSuhbP3EFQyHb3cmNpDtsrp9J/oZTh+jMUEvzQbbUaa0sRa 6 | 3pL08g9ywvtL36D6yKFEKncvdmhOyrX2Ul3iWFRbVlOwnNpjtM4o8lLqaeKaIpP9 7 | KaybDXD02i3Ob2C2GW6rd3r1LGh7lUyZXaorQQIhAIm0K4lw9cu65VTFBXga71nm 8 | p2/6zgbS0Rg9HNElB7l3AoIBAQCP/Pbu864e/WJF50Axg1GzSJCUJAA/E3G63m0a 9 | p8+dcXpg8/yEoinnRu8uyuwh/vo6kCK/hzKy5MmJJTgXsDbi/cCVEInqDAGJsoua 10 | HX2CPMMjxjUPFPb26omr9rTJYYmNs47eT/qLku9sA/0X+cOVroV3m32g2DADZiIV 11 | Nbs58rd4X9FERxM3HNMuTTwJ9Pa83F5ST5rmq1FoJpAT4iei6KV5WYmZpdGXkGD6 12 | Uj7AKreknrDRMnlNosDxl31FAj+YEpmFEvXNHJiVNRaok+CdyopTOfmYGMsV/WD9 13 | XF/mIMkqCClYI4N+kCe939IaBudLA0ih/USgxlbgrkI+JryvAoIBAFidmF18Yhyo 14 | RQPFwTUN3a35/DCDZsMypQY3K9dz9lEgjpXQpHFO/03k3ohOfy5HtRsYxaZ0S51k 15 | f9GDAG4FyiesPQCjILMs25irmDPSaKCmKnjnglKxTVukBs7lZjCSZ52RHv1vnkJH 16 | aIMoSXjs/WhvkhmLlvTQ4cjV4enS1ivVfyNBpJJkem5zXLKzFlDN2tMFMpDwf/IJ 17 | L9xUX/Cxrrk+iZQt0bHsjz0KPVzDAdYlzTjSvQV8RGc4siE/lkf1OJ2J/97XvbNz 18 | lOUnzhT0mqaXsDVaxRX9Gmm6KEG3hJG5YZwGLkzUotFv865mZF5UTY0qjGZKNZzR 19 | uVb0UGtYknwCIHEUyrhPxF3ZGswUHFO07rmKmRNZGDo9tcVelMVKpR0h 20 | -----END DSA PRIVATE KEY----- 21 | -------------------------------------------------------------------------------- /wetland/output_plugin/bearychat.py: -------------------------------------------------------------------------------- 1 | import json 2 | import requests 3 | from wetland import config 4 | from wetland.config import args 5 | 6 | 7 | # sensor name 8 | name = config.cfg.get("wetland", "name") 9 | 10 | # urls to report 11 | urls = [v for k, v in config.cfg.items("bearychat") if k.startswith("url")] 12 | 13 | 14 | class plugin(object): 15 | def __init__(self, server): 16 | self.server = server 17 | 18 | def send(self, subject, action, content): 19 | if subject != 'wetland' and 'sftp' not in subject: 20 | return False 21 | if action in ['env_request', 'pty_request', 'global_request', 22 | 'channel_request']: 23 | return False 24 | 25 | text = [] 26 | text.append('Sensor:\t%s' % name) 27 | text.append('Hacker:\t%s' % self.server.hacker_ip) 28 | text.append('MyIP:\t%s' % args.myip) 29 | text.append('Action:\t%s' % action) 30 | text.append('Content:\t%s' % content) 31 | 32 | body = {'text': '\n'.join(text), 'markdown': True, 33 | 'notification': 'Wetland Honeypot Report'} 34 | headers = {"Content-Type": "application/json"} 35 | data = json.dumps(body) 36 | 37 | for url in urls: 38 | requests.post(url, headers=headers, data=data) 39 | 40 | return True 41 | -------------------------------------------------------------------------------- /wetland/output_plugin/mqtt.py: -------------------------------------------------------------------------------- 1 | import json 2 | import datetime 3 | from wetland.config import args 4 | 5 | 6 | class plugin(object): 7 | def __init__(self, server): 8 | self.server = server 9 | 10 | def send(self, subject, action, content): 11 | t = datetime.datetime.utcnow().isoformat() 12 | 13 | if subject == 'wetland' and \ 14 | action in ('login_successful', 'shell command', 'exec command', 15 | 'direct_request', 'reverse_request', 'download'): 16 | pass 17 | 18 | elif subject in ('sftpfile', 'sftpserver'): 19 | pass 20 | 21 | elif subject == 'content' and action in ('pwd',): 22 | pass 23 | 24 | elif subject == 'upfile': 25 | pass 26 | 27 | # do not log to server 28 | else: 29 | return True 30 | 31 | data = {'timestamp': t, 'src_ip': self.server.hacker_ip, 32 | 'dst_ip': args.myip, 'action': action, 33 | 'content': content, 'sensor': args.sensor, 34 | 'src_port': self.server.hacker_port, 35 | 'dst_port': args.listen_port, 'honeypot': 'wetland', 36 | 'session': self.server.sessionuid} 37 | data = json.dumps(data) 38 | args.mqttclient.publish('ck/log/wetland', data, qos=1) 39 | return True 40 | -------------------------------------------------------------------------------- /paramiko/compress.py: -------------------------------------------------------------------------------- 1 | # Copyright (C) 2003-2007 Robey Pointer 2 | # 3 | # This file is part of paramiko. 4 | # 5 | # Paramiko is free software; you can redistribute it and/or modify it under the 6 | # terms of the GNU Lesser General Public License as published by the Free 7 | # Software Foundation; either version 2.1 of the License, or (at your option) 8 | # any later version. 9 | # 10 | # Paramiko is distributed in the hope that it will be useful, but WITHOUT ANY 11 | # WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR 12 | # A PARTICULAR PURPOSE. See the GNU Lesser General Public License for more 13 | # details. 14 | # 15 | # You should have received a copy of the GNU Lesser General Public License 16 | # along with Paramiko; if not, write to the Free Software Foundation, Inc., 17 | # 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA. 18 | 19 | """ 20 | Compression implementations for a Transport. 21 | """ 22 | 23 | import zlib 24 | 25 | 26 | class ZlibCompressor (object): 27 | def __init__(self): 28 | self.z = zlib.compressobj(9) 29 | 30 | def __call__(self, data): 31 | return self.z.compress(data) + self.z.flush(zlib.Z_FULL_FLUSH) 32 | 33 | 34 | class ZlibDecompressor (object): 35 | def __init__(self): 36 | self.z = zlib.decompressobj() 37 | 38 | def __call__(self, data): 39 | return self.z.decompress(data) 40 | -------------------------------------------------------------------------------- /wetland/services/visual.txt: -------------------------------------------------------------------------------- 1 | {"\u0000": "(NUL)", "\u0004": "(EOT)", "\b": "(BS)", "\f": "(FF)", "\u0010": "(DLE)", "\u0014": "(DC4)", "\u0018": "(CAN)", "\u001c": "(FS)", " ": " ", "$": "$", "(": "(", ",": ",", "0": "0", "4": "4", "8": "8", "<": "<<", "@": "@", "D": "D", "H": "H", "L": "L", "P": "P", "T": "T", "X": "X", "\\": "\\", "`": "`", "d": "d", "h": "h", "l": "l", "p": "p", "t": "t", "x": "x", "|": "|", "\u0003": "(ETX)", "\u0007": "(BEL)", "\u000b": "(VT)", "\u000f": "(SI)", "\u0013": "(DC3)", "\u0017": "(ETB)", "\u001b": "(ESC)", "\u001f": "(US)", "#": "#", "'": "'", "+": "+", "/": "/", "3": "3", "7": "7", ";": ";", "?": "?", "C": "C", "G": "G", "K": "K", "O": "O", "S": "S", "W": "W", "[": "[", "_": "_", "c": "c", "g": "g", "k": "k", "o": "o", "s": "s", "w": "w", "{": "{", "\u007f": "(DEL)", "\u0002": "(STX)", "\u0006": "(ACK)", "\n": "\n", "\u000e": "(SO)", "\u0012": "(DC2)", "\u0016": "(SYN)", "\u001a": "(SUB)", "\u001e": "(RS)", "\"": "\"", "&": "&", "*": "*", ".": ".", "2": "2", "6": "6", ":": ":", ">": ">", "B": "B", "F": "F", "J": "J", "N": "N", "R": "R", "V": "V", "Z": "Z", "^": "^", "b": "b", "f": "f", "j": "j", "n": "n", "r": "r", "v": "v", "z": "z", "~": "~", "\u0001": "(SOH)", "\u0005": "(ENQ)", "\t": "(HT)", "\r": "\n", "\u0011": "(DC1)", "\u0015": "(NAK)", "\u0019": "(EM)", "\u001d": "(GS)", "!": "!", "%": "%", ")": ")", "-": "-", "1": "1", "5": "5", "9": "9", "=": "=", "A": "A", "E": "E", "I": "I", "M": "M", "Q": "Q", "U": "U", "Y": "Y", "]": "]", "a": "a", "e": "e", "i": "i", "m": "m", "q": "q", "u": "u", "y": "y", "}": "}"} 2 | -------------------------------------------------------------------------------- /wetland/services/direct_service.py: -------------------------------------------------------------------------------- 1 | import time 2 | import select 3 | 4 | 5 | def direct_service(hacker_channel_id, hacker_trans, docker_channel, output): 6 | 7 | for i in range(10): 8 | if hacker_trans._channels.get(hacker_channel_id): 9 | break 10 | time.sleep(1) 11 | else: 12 | # print 'direct wait for channel timeout' 13 | docker_channel.close() 14 | return 15 | 16 | hacker_channel = hacker_trans._channels.get(hacker_channel_id) 17 | output.o('content', 'direct', "N"*20) 18 | 19 | try: 20 | while True: 21 | 22 | r, w, x = select.select([hacker_channel, docker_channel], [], []) 23 | if hacker_channel in r: 24 | text = hacker_channel.recv(1024) 25 | output.o('content', 'direct', '[H]:'+text.encode("hex")) 26 | docker_channel.send(text) 27 | 28 | if docker_channel in r: 29 | text = docker_channel.recv(1024) 30 | output.o('content', 'direct', '[V]:'+text.encode("hex")) 31 | hacker_channel.send(text) 32 | 33 | if docker_channel.eof_received: 34 | hacker_channel.shutdown_write() 35 | hacker_channel.send_exit_status(0) 36 | 37 | if hacker_channel.eof_received: 38 | docker_channel.shutdown_write() 39 | docker_channel.send_exit_status(0) 40 | 41 | if docker_channel.eof_received and hacker_channel.eof_received: 42 | break 43 | 44 | except Exception, e: 45 | print e 46 | finally: 47 | hacker_channel.close() 48 | docker_channel.close() 49 | -------------------------------------------------------------------------------- /wetland/output_plugin/log.py: -------------------------------------------------------------------------------- 1 | import os 2 | import time 3 | import paramiko 4 | from wetland import config 5 | 6 | # path of log files 7 | logpath = config.cfg.get("log", "path") 8 | if not os.path.exists(logpath): 9 | os.makedirs(logpath) 10 | 11 | # paramiko.log maybe too large 12 | # paramiko.util.log_to_file(logpath + '/paramiko.log') 13 | 14 | # sensor name 15 | name = config.cfg.get("wetland", "name") 16 | 17 | 18 | class plugin(object): 19 | def __init__(self, server): 20 | self.server = server 21 | self.logpath = None 22 | self.get_logpath() 23 | 24 | def get_logpath(self): 25 | self.logpath = os.path.join(logpath, self.server.hacker_ip) 26 | if not os.path.exists(self.logpath): 27 | os.makedirs(self.logpath) 28 | 29 | def send(self, subject, action, content): 30 | if subject == 'wetland': 31 | with open(os.path.join(self.logpath, 'wetland.log'), 'a') as logfile: 32 | log = ' '.join([time.strftime("%y%m%d-%H:%M:%S"), 33 | action, content, '\n']) 34 | logfile.write(log) 35 | 36 | elif subject == 'content': 37 | with open(os.path.join(self.logpath, action+'.log'), 'a') as logfile: 38 | log = ' '.join([time.strftime("%y%m%d-%H:%M:%S"), 39 | content, '\n']) 40 | logfile.write(log) 41 | 42 | elif subject in ['sftpfile', 'sftpserver']: 43 | with open(os.path.join(self.logpath, 'sftp.log'), 'a') as logfile: 44 | log = ' '.join([time.strftime("%y%m%d-%H:%M:%S"), 45 | action, content, '\n']) 46 | logfile.write(log) 47 | -------------------------------------------------------------------------------- /data/id_rsa: -------------------------------------------------------------------------------- 1 | -----BEGIN RSA PRIVATE KEY----- 2 | MIIEpQIBAAKCAQEA0s7K0JEAh6RgErqel3+vPO31jSXIrE3PK4cBOIiEEdJ5GGF3 3 | CQqwuCy/tojtxLLP/J01siIoQPVxqHPakY35mnIkG/8Or1vQBod2DyZfsNRsuU2J 4 | LVln8SoWda4OZmJ8EXQ7TJBCfzVDoDsMtVK3CSiUVLavISSF6N7oqTFP0Iz6JId2 5 | FHLmPnks51rpZAuNoNjztgCtUEsLnZho1G7tgn2o+zlMPojA53UQ2O5KABb+5PGU 6 | Xi93GVXNUoWeeEWXdC8BPfgzNgbt7uqTvxkzfKkftokxZ6fgD4cZup8162dZjr1p 7 | UZ2YF/5iA6xSCWRRm5xx606Pr5CIxYQeyRxbSQIDAQABAoIBAA7OXWpWtN3SCrlm 8 | CBuF6U3zPKrkO3r2oBVjjw9kbzo9OSogNlcZPB5DbleouGJ3WKpadFoFkiQ12PzG 9 | 3lQYz64sVCLvQB3K6gp7WyxnOpNV6Cj/rCHWRLmohhlP100Tn7kuG7gd7b23hP9t 10 | BlB59ccb5YcpqZU/VnRrq5Wuk/eGvqXuPi87IysohQ2jqt649ZX2Hjhh645EJm/w 11 | ZJnhDTQjiIYx20uboexsjuKZATPoRIEs2lc1A6uiF7Lf7fHaMTe7QZJIPEt78W0C 12 | Up8pLNqJMyKqitcs6FjmshEdlwk4yk4bBg7AzWdULQWBK3cbFaIaqSWLurr662oh 13 | P0m3MB0CgYEA8gWOe4e0h79XvZXNMo/ihAXyZxKcuinrlik+SufY99UGQRcejLQk 14 | VnzGrJqAmuPqEQsJFWX5lLV59MH0JdY21kv0AK1A6/lltHUowwnTlRyOmx11FbyW 15 | Ptzm/rYI4oV2iLNyuuztnJzSuT1P0diIP5GFyAH1sqz4LYCOALkq2dcCgYEA3vu3 16 | 2JkET8wtC/rO7F18RwJrEmwZtL0Nf1ljQ3Mj1MxK0hOwxcyiglQTCga1Gx78WoAD 17 | VGuJwoqxHU7LmZHdpEv3FA55ZrskieYcYOSrAXGCJg7yUjABP+FsfuoiFIHQXMrB 18 | aX9kDtQDNQAUetYemgKCJ8JBi35N66VAV2eDD98CgYEAxLD7YS2i7If3ON7cC3Gg 19 | eAeVP2uC8FFNY5ZVR+8xCAmasZ+mdleZCKkTlgdi6X69JejAEyHzOoZafS57y5xS 20 | qMrFnR8xGhZwL77fx9Len9q7kxjXpTjFKoXBPdSXV/F7qhGc9onDenqBT4airjq6 21 | UF9mSGw/UFz+vYwy3CegybUCgYEAhhT2x9e7MACmVb3LD3ZndIuPttQp4PSNWTZ2 22 | 6egic6Mkmo4cjdQvJA9KI41E+bn4JLM1TV4cyE59khH/e6iqjlDfkb/iYFPH9OPZ 23 | zKmz6npGuHvkWmdjWPZEN0yykYI9uI5zHuzrTb9O7l/N8M8wN4uqmB3HLAoW8Mu9 24 | lNE5jiECgYEAxxYDGzHH1iAVG4roSZuhpFoWXvoB2GH2+YsTAXLsRjxZfEYpviPh 25 | Lt3NRT7T0/BuR8RLGsSg3PQcpksqlObEgqPPF1dw8o07cBi3m5su3BkvrLC67Q3L 26 | ItW6i9ufjnkL236aQ3mHcItOvC7P6XNE+QD0zghjJaO2d0MmWXIj3iE= 27 | -----END RSA PRIVATE KEY----- 28 | -------------------------------------------------------------------------------- /paramiko/kex_group14.py: -------------------------------------------------------------------------------- 1 | # Copyright (C) 2013 Torsten Landschoff 2 | # 3 | # This file is part of paramiko. 4 | # 5 | # Paramiko is free software; you can redistribute it and/or modify it under the 6 | # terms of the GNU Lesser General Public License as published by the Free 7 | # Software Foundation; either version 2.1 of the License, or (at your option) 8 | # any later version. 9 | # 10 | # Paramiko is distrubuted in the hope that it will be useful, but WITHOUT ANY 11 | # WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR 12 | # A PARTICULAR PURPOSE. See the GNU Lesser General Public License for more 13 | # details. 14 | # 15 | # You should have received a copy of the GNU Lesser General Public License 16 | # along with Paramiko; if not, write to the Free Software Foundation, Inc., 17 | # 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA. 18 | 19 | """ 20 | Standard SSH key exchange ("kex" if you wanna sound cool). Diffie-Hellman of 21 | 2048 bit key halves, using a known "p" prime and "g" generator. 22 | """ 23 | 24 | from paramiko.kex_group1 import KexGroup1 25 | from hashlib import sha1 26 | 27 | 28 | class KexGroup14(KexGroup1): 29 | 30 | # http://tools.ietf.org/html/rfc3526#section-3 31 | P = 0xFFFFFFFFFFFFFFFFC90FDAA22168C234C4C6628B80DC1CD129024E088A67CC74020BBEA63B139B22514A08798E3404DDEF9519B3CD3A431B302B0A6DF25F14374FE1356D6D51C245E485B576625E7EC6F44C42E9A637ED6B0BFF5CB6F406B7EDEE386BFB5A899FA5AE9F24117C4B1FE649286651ECE45B3DC2007CB8A163BF0598DA48361C55D39A69163FA8FD24CF5F83655D23DCA3AD961C62F356208552BB9ED529077096966D670C354E4ABC9804F1746C08CA18217C32905E462E36CE3BE39E772C180E86039B2783A2EC07A28FB5C55DF06F4C52C9DE2BCBF6955817183995497CEA956AE515D2261898FA051015728E5A8AACAA68FFFFFFFFFFFFFFFF # noqa 32 | G = 2 33 | 34 | name = 'diffie-hellman-group14-sha1' 35 | hash_algo = sha1 36 | -------------------------------------------------------------------------------- /wetland/services/reverse_service.py: -------------------------------------------------------------------------------- 1 | import select 2 | import threading 3 | 4 | 5 | def reverse_handler(*args): 6 | t = threading.Thread(target=reverse_handler2, args=(args)) 7 | t.setDaemon(True) 8 | t.start() 9 | 10 | 11 | def reverse_handler2(docker_channel, origin, destination, hacker_trans, 12 | output): 13 | try: 14 | hacker_channel = hacker_trans.open_forwarded_tcpip_channel(origin, 15 | destination) 16 | except Exception: 17 | docker_channel.close() 18 | return 19 | 20 | output.o('wetland', 'reverse', 'ori:%s, dest:%s' % (origin, destination)) 21 | output.o('content', 'reverse', "N"*20) 22 | 23 | try: 24 | while True: 25 | r, w, x = select.select([hacker_channel, docker_channel], [], []) 26 | if hacker_channel in r: 27 | text = hacker_channel.recv(1024) 28 | output.o('content', 'reverse', '[H]:'+text.encode("hex")) 29 | docker_channel.send(text) 30 | 31 | if docker_channel in r: 32 | text = docker_channel.recv(1024) 33 | output.o('content', 'reverse', '[V]:'+text.encode("hex")) 34 | hacker_channel.send(text) 35 | 36 | if docker_channel.eof_received: 37 | hacker_channel.shutdown_write() 38 | hacker_channel.send_exit_status(0) 39 | 40 | if hacker_channel.eof_received: 41 | docker_channel.shutdown_write() 42 | docker_channel.send_exit_status(0) 43 | 44 | if docker_channel.eof_received and hacker_channel.eof_received: 45 | break 46 | 47 | except Exception as e: 48 | print e 49 | finally: 50 | hacker_channel.close() 51 | docker_channel.close() 52 | -------------------------------------------------------------------------------- /util/initwetland.py: -------------------------------------------------------------------------------- 1 | import os 2 | import sys 3 | import shutil 4 | 5 | import paramiko 6 | import subprocess 7 | 8 | # Get root path 9 | if len(sys.argv) == 1: 10 | print '[-] please specify wetland\'s root path' 11 | sys.exit(1) 12 | path = sys.argv[1] 13 | print '[+] Root Path: \t%s' % path 14 | 15 | if not os.path.exists(path): 16 | print '[-] Not Exists: \t root path' 17 | sys.exit(1) 18 | 19 | os.chdir(path) 20 | 21 | 22 | # Save files 23 | if not os.path.exists('files'): 24 | os.mkdir('files') 25 | 26 | 27 | # Generate keys 28 | print '[+] Checking: \tdata folder' 29 | if os.path.exists('data'): 30 | print '[+] Exists: \tfolder data' 31 | else: 32 | print '[+] Creating: \tfolder data' 33 | os.mkdir('data') 34 | 35 | if os.path.exists(os.path.join(path, 'data', 'id_rsa')): 36 | print '[+] Exists: \tid_rsa' 37 | else: 38 | print '[+] Creating: \tid_rsa' 39 | key = paramiko.RSAKey.generate(2048) 40 | key.write_private_key_file(os.path.join(path, 'data', 'id_rsa')) 41 | 42 | if os.path.exists(os.path.join(path, 'data', 'id_dsa')): 43 | print '[+] Exists: \tid_dsa' 44 | else: 45 | print '[+] Creating: \tid_dsa' 46 | key = paramiko.DSSKey.generate(2048) 47 | key.write_private_key_file(os.path.join(path, 'data', 'id_dsa')) 48 | 49 | 50 | # Install python dependency 51 | if not os.path.exists('requirements'): 52 | print '[-] Not found: \trequirements' 53 | sys.exit(1) 54 | 55 | print '[+] Installing:\tpython dependency' 56 | s = subprocess.Popen("pip install -r requirements", shell=True, 57 | stdout=subprocess.PIPE) 58 | s.communicate() 59 | if s.returncode: 60 | print '[-] pip error' 61 | sys.exit(1) 62 | 63 | 64 | # Clean root folder 65 | print '[+] Cleaning: \troot folder' 66 | 67 | print '[+] Moving: \tdocuments into folder data' 68 | shutil.move('README.md', 'data') 69 | shutil.move('requirements', 'data') 70 | 71 | print '[+] Copying: \tcfg.default to cfg' 72 | shutil.copy('wetland.cfg.default', 'wetland.cfg') 73 | shutil.move('wetland.cfg.default', 'data') 74 | -------------------------------------------------------------------------------- /util/clearlog.py: -------------------------------------------------------------------------------- 1 | import os 2 | import time 3 | import argparse 4 | 5 | valueSet = set(['shell.og', 'reverse.log', 'direct.log', 'exec.log']) 6 | 7 | 8 | def clear(logs_path, pwd): 9 | for ip in os.listdir(logs_path): 10 | if not os.path.isdir(os.path.join(logs_path, ip)): 11 | continue 12 | 13 | log = os.listdir(os.path.join(logs_path, ip)) 14 | 15 | if 'pwd.log' in log: 16 | pwd_path = os.path.join(logs_path, ip, 'pwd.log') 17 | with open(pwd, 'a') as w, open(pwd_path) as r: 18 | for i in r.readlines(): 19 | if i: 20 | try: 21 | w.writelines(i.split(" ")[1] + '\n') 22 | except: 23 | print i 24 | 25 | if (not len(set(log) & valueSet)) and ip != 'sftp': 26 | os.system('rm -rf %s' % os.path.join(logs_path, ip)) 27 | 28 | 29 | def summary(logs_path): 30 | timeformat = '%y%m%d-%H:%M' 31 | summary_log = open(os.path.join(logs_path, 'summary.txt'), 'w') 32 | 33 | for root, dirs, files in os.walk(logs_path): 34 | if root == logs_path: 35 | continue 36 | 37 | summary_log.write(root + '\n') 38 | for f in files: 39 | p = os.path.join(root, f) 40 | t = time.strftime(timeformat, time.localtime(os.stat(p).st_mtime)) 41 | s = os.stat(p).st_size/1024.0 42 | if s < 1: 43 | summary_log.write('\t%s %.2fkb \t%s\n' % (t, s, f)) 44 | else: 45 | summary_log.write('\t%s %dkb \t %s\n' % (t, s, f)) 46 | 47 | summary_log.close() 48 | 49 | 50 | if __name__ == '__main__': 51 | parser = argparse.ArgumentParser(description='Delete pwdlog, Leave others') 52 | parser.add_argument("p", help='The path of log folder') 53 | parser.add_argument("-l", help='The path of pwd file') 54 | args = parser.parse_args() 55 | if not args.l: 56 | args.l = os.path.join(args.p, 'pwd.txt') 57 | 58 | clear(args.p, args.l) 59 | summary(args.p) 60 | -------------------------------------------------------------------------------- /wetland.cfg.default: -------------------------------------------------------------------------------- 1 | [wetland] 2 | # IP addresses to listen for incoming SSH connections. 3 | wetland_addr = 0.0.0.0 4 | wetland_port = 22 5 | req_public_ip = false 6 | 7 | # IP address to send outgoing SSH connections. 8 | docker_addr = 172.17.0.2 9 | docker_port = 22 10 | 11 | # Name of this sensor, used when logging or outputing 12 | name = Wetland HoneyPot 1 13 | 14 | whitelist = false 15 | blacklist = false 16 | 17 | [ssh] 18 | # Generate wth `ssh-keygen -t rsa` 19 | private_rsa = data/id_rsa 20 | 21 | # Generate with `ssh-keygen -t dsa` 22 | private_dsa = data/id_dsa 23 | 24 | # Banner of wetland server 25 | # It should be same as with banner of sshd docker 26 | banner = SSH-2.0-OpenSSH_7.2p2 Ubuntu-4ubuntu2.2 27 | 28 | 29 | [files] 30 | # The folder to store the files uploaded by hackers. 31 | path = files/ 32 | 33 | [network] 34 | # Enable advanced network, wetland must has root permissions 35 | enable = false 36 | 37 | [output] 38 | # Ways to report to you when wetland visited by hackers. 39 | # except log, others only report wetland log 40 | p0fp0f = false 41 | log = false 42 | jsonlog = false 43 | email = false 44 | bearychat = false 45 | mqtt = false 46 | 47 | [mqtt] 48 | # host = xxx 49 | # usr = root 50 | # pwd = pwd 51 | 52 | [jsonlog] 53 | 54 | # file store jsons 55 | # file = log/wetlandjson.log 56 | # log to this tcp socket 57 | # tcp = 1.1.1.1:1234 58 | # log to this udp socket 59 | # udp = 2.2.2.2:1234 60 | 61 | [bearychat] 62 | # Urls of bearychat incoming robots 63 | # Each of them should be start with url, e.g. url1 url2 64 | url1 = https://hook.bearychat.com/xxx 65 | 66 | 67 | [email] 68 | #user, pwd, host, port of your smtp server 69 | user = example@163.com 70 | pwd = example 71 | host = smtp.163.com 72 | port = 25 73 | 74 | # Wetland can send message to mutli emails 75 | # It should start with 'to', e.g. to1 to2 to3 76 | to1 = example@qq.com 77 | 78 | 79 | [log] 80 | # The folder to store logs 81 | path = log 82 | 83 | 84 | [p0fp0f] 85 | # Path to p0f folder 86 | path = p0f 87 | # iface to sniff to sniff 88 | iface = eth0 89 | # sock name of p0f api 90 | sockname = wetland 91 | -------------------------------------------------------------------------------- /wetland/output_plugin/jsonlog.py: -------------------------------------------------------------------------------- 1 | import json 2 | import datetime 3 | import socket 4 | from wetland import config 5 | 6 | 7 | class plugin(object): 8 | def __init__(self, server): 9 | self.server = server 10 | self.methods = list(set(['file', 'tcp', 'udp']) & 11 | set(config.cfg.options('jsonlog'))) 12 | self.name = config.cfg.get("wetland", "name") 13 | 14 | if 'tcp' in self.methods: 15 | ip, port = config.cfg.get('jsonlog', 'tcp').split(':') 16 | port = int(port) 17 | self.tcpsock = (ip, port) 18 | if 'udp' in self.methods: 19 | ip, port = config.cfg.get('jsonlog', 'udp').split(':') 20 | port = int(port) 21 | self.udpsock = (ip, port) 22 | if 'file' in self.methods: 23 | self.logfile = config.cfg.get('jsonlog', 'file') 24 | 25 | def file(self, data): 26 | with open(self.logfile, 'a') as logfile: 27 | logfile.write(data+'\n') 28 | 29 | def udp(self, data): 30 | s = socket.socket(socket.AF_INET, socket.SOCK_DGRAM) 31 | s.connect(self.udpsock) 32 | s.send(data) 33 | s.close() 34 | 35 | def tcp(self, data): 36 | s = socket.socket(socket.AF_INET, socket.SOCK_STREAM) 37 | s.connect(self.tcpsock) 38 | s.send(data) 39 | s.close() 40 | 41 | def send(self, subject, action, content): 42 | t = datetime.datetime.utcnow().isoformat() 43 | 44 | if subject == 'wetland' and \ 45 | action in ('login_successful', 'shell command', 'exec command', 46 | 'direct_request', 'reverse_request'): 47 | pass 48 | 49 | elif subject in ('sftpfile', 'sftpserver'): 50 | pass 51 | 52 | elif subject == 'content' and action in ('pwd',): 53 | pass 54 | 55 | elif subject == 'upfile': 56 | pass 57 | 58 | else: 59 | return True 60 | 61 | data = {'timestamp': t, 'src_ip': self.server.hacker_ip, 62 | 'dst_ip': self.server.myip, 'action': action, 63 | 'content': content, 'sensor': self.name, 64 | 'src_port': self.server.hacker_port, 65 | 'dst_port': 22} 66 | data = json.dumps(data) + '\n' 67 | for m in self.methods: 68 | getattr(self, m)(data) 69 | return True 70 | -------------------------------------------------------------------------------- /wetland/config.py: -------------------------------------------------------------------------------- 1 | import socket 2 | from os.path import join 3 | import paho.mqtt.client as mqtt 4 | import ConfigParser 5 | 6 | cfg = ConfigParser.ConfigParser() 7 | cfg.read('wetland.cfg') 8 | 9 | 10 | class DottableDict(dict): 11 | def __init__(self, *args, **kwargs): 12 | dict.__init__(self, *args, **kwargs) 13 | self.__dict__ = self 14 | 15 | def allowDotting(self, state=True): 16 | if state: 17 | self.__dict__ = self 18 | else: 19 | self.__dict__ = dict() 20 | 21 | 22 | def reqpubip(listen_ip=None): 23 | outip = None 24 | inip = None 25 | 26 | while 1: 27 | try: 28 | s = socket.socket(socket.AF_INET, socket.SOCK_STREAM) 29 | s.settimeout(20) 30 | s.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1) 31 | if listen_ip: 32 | s.bind((listen_ip, 0)) 33 | s.connect(('icanhazip.com', 80)) 34 | s.send("GET / HTTP/1.1\r\n" 35 | "Host:icanhazip.com\r\n" 36 | "User-Agent:curl\r\n\r\n") 37 | outip = s.recv(1024).split("\r\n")[-1].split('\n')[0] 38 | inip = s.getsockname()[0] 39 | return outip, inip 40 | except Exception, e: 41 | print 51, e 42 | finally: 43 | s.close() 44 | 45 | 46 | def get_args(): 47 | args = DottableDict() 48 | 49 | # mqtt client 50 | if cfg.getboolean('output', 'mqtt'): 51 | # args.keys_path = cfg.get('mqtt', 'keys_path') 52 | # ca_certs = join(args.keys_path, 'ca.crt') 53 | # cert_file = join(args.keys_path, 'client.crt') 54 | # key_file = join(args.keys_path, 'client.key') 55 | 56 | args.mqtthost = cfg.get('mqtt', 'host') 57 | mqttclient = mqtt.Client() 58 | # mqttclient.tls_set(ca_certs=ca_certs, 59 | # certfile=cert_file, 60 | # keyfile=key_file) 61 | mqttclient.username_pw_set(cfg.get('mqtt', 'usr'), 62 | cfg.get('mqtt', 'pwd')) 63 | 64 | mqttclient.connect_async(args.mqtthost) 65 | mqttclient.loop_start() 66 | 67 | args.mqttclient = mqttclient 68 | 69 | if cfg.getboolean('wetland', 'req_public_ip'): 70 | args.myip, args.listen_ip = reqpubip() 71 | else: 72 | _, args.myip = reqpubip() 73 | args.sensor = cfg.get('wetland', 'name') 74 | args.listen_port = cfg.getint('wetland', 'wetland_port') 75 | 76 | return args 77 | 78 | 79 | args = get_args() 80 | -------------------------------------------------------------------------------- /wetland/services/shell_service.py: -------------------------------------------------------------------------------- 1 | import os 2 | import json 3 | import select 4 | 5 | with open(os.path.join(os.path.dirname(__file__), 'visual.txt')) as txt: 6 | visual = json.load(txt) 7 | 8 | 9 | def shell_service(hacker_session, docker_session, output): 10 | 11 | hacker_session.settimeout(3600) 12 | output.o('content', 'shell', "N"*20) 13 | 14 | hacker_fileno = hacker_session.fileno() 15 | docker_fileno = docker_session.fileno() 16 | 17 | epoll = select.epoll() 18 | epoll.register(hacker_fileno, select.EPOLLIN) 19 | epoll.register(docker_fileno, select.EPOLLIN) 20 | 21 | try: 22 | command = [] 23 | while True: 24 | events = epoll.poll() 25 | 26 | for fd, event in events: 27 | if fd == hacker_fileno: 28 | 29 | if event & select.EPOLLIN: 30 | if hacker_session.recv_ready(): 31 | text = hacker_session.recv(1) 32 | docker_session.sendall(text) 33 | output.o('content', 'shell', 34 | '[H]:'+text.encode("hex")) 35 | if text == '\r': 36 | cmd = ''.join(command) 37 | output.o('wetland', 'shell command', cmd) 38 | command = [] 39 | elif text == '\x7f' and command: 40 | command.pop() 41 | else: 42 | command.append(visual[text]) 43 | if hacker_session.eof_received: 44 | docker_session.shutdown_write() 45 | docker_session.send_exit_status(0) 46 | elif event & select.EPOLLHUP: 47 | break 48 | 49 | elif fd == docker_fileno: 50 | if event & select.EPOLLIN: 51 | if docker_session.recv_ready(): 52 | text = docker_session.recv(1024) 53 | output.o('content', 'shell', 54 | '[V]:'+text.encode("hex")) 55 | hacker_session.sendall(text) 56 | 57 | if docker_session.recv_stderr_ready(): 58 | text = docker_session.recv_stderr(1024) 59 | hacker_session.sendall_stderr(text) 60 | 61 | if docker_session.eof_received: 62 | hacker_session.shutdown_write() 63 | hacker_session.send_exit_status(0) 64 | elif event & select.EPOLLHUP: 65 | break 66 | if docker_session.eof_received or hacker_session.eof_received: 67 | break 68 | 69 | except Exception, e: 70 | print e 71 | finally: 72 | hacker_session.close() 73 | docker_session.close() 74 | -------------------------------------------------------------------------------- /wetland/server/tcpServer.py: -------------------------------------------------------------------------------- 1 | import json 2 | import uuid 3 | import paramiko 4 | from collections import defaultdict 5 | 6 | from wetland.services import SocketServer 7 | from wetland.server import sshServer 8 | from wetland.server import sftpServer 9 | from wetland.config import cfg, args 10 | 11 | 12 | class tcp_server(SocketServer.ThreadingTCPServer): 13 | daemon_threads = True 14 | allow_reuse_address = True 15 | 16 | def __init__(self, sock, handler): 17 | super(tcp_server, self).__init__(sock, handler) 18 | self.cfg = cfg 19 | 20 | self.whitelist = None 21 | self.blacklist = defaultdict(lambda: 0) 22 | self.sessions = {} 23 | 24 | if self.cfg.getboolean('wetland', 'whitelist') and \ 25 | self.cfg.getboolean('output', 'mqtt'): 26 | 27 | self.whitelist = ['127.0.0.1'] 28 | 29 | def on_connect(client, userdata, flags, rc): 30 | client.subscribe("ck/ctr/wetland/whitelist") 31 | 32 | def on_message(client, userdata, msg): 33 | newwhitelist = json.loads(msg.payload) 34 | # runtime update the whitelist in sshserver 35 | for _ in range(len(self.whitelist)): 36 | del self.whitelist[0] 37 | for i in newwhitelist: 38 | self.whitelist.append(i) 39 | print 'whitelist', self.whitelist 40 | 41 | args.mqttclient.subscribe("ck/ctr/wetland/whitelist") 42 | args.mqttclient.on_connect = on_connect 43 | args.mqttclient.on_message = on_message 44 | 45 | 46 | class tcp_handler(SocketServer.BaseRequestHandler): 47 | def handle(self): 48 | transport = paramiko.Transport(self.request) 49 | 50 | rsafile = self.server.cfg.get("ssh", "private_rsa") 51 | dsafile = self.server.cfg.get("ssh", "private_dsa") 52 | rsakey = paramiko.RSAKey(filename=rsafile) 53 | dsakey = paramiko.DSSKey(filename=dsafile) 54 | transport.add_server_key(rsakey) 55 | transport.add_server_key(dsakey) 56 | 57 | transport.local_version = self.server.cfg.get("ssh", "banner") 58 | 59 | transport.set_subsystem_handler('sftp', paramiko.SFTPServer, 60 | sftpServer.sftp_server) 61 | 62 | hacker_addr = transport.getpeername()[0] 63 | if hacker_addr not in self.server.sessions: 64 | uid = uuid.uuid4().get_hex() 65 | self.server.sessions[hacker_addr] = uid 66 | else: 67 | uid = self.server.sessions[hacker_addr] 68 | sServer = sshServer.ssh_server(transport=transport, 69 | whitelist=self.server.whitelist, 70 | blacklist=self.server.blacklist, 71 | sessionuid=uid) 72 | 73 | try: 74 | transport.start_server(server=sServer) 75 | except paramiko.SSHException: 76 | return 77 | except Exception as e: 78 | print e 79 | sServer.docker_trans.close() 80 | return 81 | 82 | try: 83 | while True: 84 | chann = transport.accept(60) 85 | # no channel left 86 | if not transport._channels.values(): 87 | break 88 | except Exception as e: 89 | print e 90 | finally: 91 | sServer.docker_trans.close() 92 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Wetland 2 | Wetland is a high interaction SSH honeypot,designed to log brute force attacks.What's more, wetland will log shell、scp、sftp、exec-command、direct-forward、reverse-forward interation performded by the attacker. 3 | 4 | Wetland is based on python ssh module [paramiko](https://github.com/paramiko/paramiko/). And wetland runs as a multi-threading tcp server using SocketServer. 5 | 6 | ## Features 7 | * Use docker to provide a real linux environment. 8 | * All the password auth will redirect to docker. 9 | * All the command will execute on docker. 10 | * Save a copy of file when hacker uploads some files with SFTP. 11 | * Extract and Save files from exec-log when hacker uoloads some files with SCP. 12 | * Providing a playlog script to replay the [shell | exec | direct-forward | reverse-forward] kind of log. 13 | * Advanced networking feature to spoof attackers IP address between wetland and docker(thanks to [honssh](https://github.com/tnich/honssh)) 14 | * Kinds of ways to report to you when wetland is touching by hacker, but now only email and bearychat. 15 | 16 | ## Requirements 17 | * A linux system (tested on ubuntu) 18 | * sshd images in docker (e.g rastasheep/ubuntu-sshd) 19 | * python2.7 20 | * paramiko 21 | * yagmail 22 | * IPy 23 | * requests 24 | 25 | ## Setup and Configuration 26 | 1. Copy wetland.cfg.default to wetland.cfg 27 | 2. Generate keys used by ssh server 28 | * run `mkdir data` 29 | * run `ssh-keygen -t rsa`, and put them in `data/` 30 | * run `ssh-keygen -t dsa`, and put them in `data/` 31 | * Remember that Wetland and sshd container should use the same keys. 32 | 3. Install python requirements 33 | * run `pip install -r requirements` 34 | 4. Configure the banner of ssh server 35 | * Edit banner in wetland.cfg 36 | * It should be same with the ssh banner of sshd contaniner 37 | 5. Or you can run `python util/initwetland.py ./`,this script will do all the work above 38 | 5. Configure the output plugins in wetland.cfg 39 | * enable or disable in `[output]` section 40 | * Edit the url of incoming robots when using bearychat 41 | * Edit user、pwd... when using email 42 | 6. Install p0f if you want 43 | * run `git clone https://github.com/p0f/p0f` 44 | * run `cd p0f` 45 | * run `./build.sh` 46 | * Edit `[p0fp0f]` section in wetland.cfg 47 | * if you dont need p0f, just disable p0f in [output] section 48 | 7. Install docker 49 | * install docker with docs in [www.docker.com](www.docker.com) 50 | * run `docker search sshd`, then choose a image running sshd 51 | * run `docker run -d --name sshd sshd_image_name` 52 | * run `docker inspect sshd`, then edit docker ip address and port in wetland.cfg 53 | * sshd's ssh port should be same with wetland's 54 | * delete and replace sshd container sometimes if you want 55 | 56 | ## Running 57 | 1. Run 58 | * run `nohup python main.py &` 59 | 2. Stop 60 | * run `netstat -autpn | grep 22` 61 | * then `kill pid_number` 62 | * ahaha 63 | 3. Clean 64 | * Maybe you should delete some iface created by networking module by hand. 65 | * run `ip link list` 66 | * then `ip link del dev wdxxxxxx` 67 | * finally clean up the nat table of iptables or just reboot 68 | 4. View logs 69 | * run `python util/clearlog.py -p log` will remove logs that only have pwd.log, and username:password will write into -l file, default ./pwd.txt 70 | * then use playlog.py in util 71 | 5. file system changes 72 | * filechange.py will copy the read-write layer of the sshd container to ./ , the layer includes that hacker create and remove 73 | * e.g. `docker inspect sshd --format '{{.Id}}' | python filechange.py -` 74 | 75 | ## Dockerized wetland 76 | * run `docker pull ohmyadd/wetland` 77 | * see it in [docker hub](https://hub.docker.com/r/ohmyadd/wetland/) 78 | 79 | ## TODO 80 | * wetland dockerized 81 | * create sshd docker image realistic 82 | * automate create sshd container 83 | 84 | 85 | * add watchdog 86 | * take use of bearychat incoming outgoing 87 | * distribute log system & support hpfeeds 88 | -------------------------------------------------------------------------------- /paramiko/__init__.py: -------------------------------------------------------------------------------- 1 | # Copyright (C) 2003-2011 Robey Pointer 2 | # 3 | # This file is part of paramiko. 4 | # 5 | # Paramiko is free software; you can redistribute it and/or modify it under the 6 | # terms of the GNU Lesser General Public License as published by the Free 7 | # Software Foundation; either version 2.1 of the License, or (at your option) 8 | # any later version. 9 | # 10 | # Paramiko is distributed in the hope that it will be useful, but WITHOUT ANY 11 | # WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR 12 | # A PARTICULAR PURPOSE. See the GNU Lesser General Public License for more 13 | # details. 14 | # 15 | # You should have received a copy of the GNU Lesser General Public License 16 | # along with Paramiko; if not, write to the Free Software Foundation, Inc., 17 | # 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA. 18 | 19 | # flake8: noqa 20 | import sys 21 | from paramiko._version import __version__, __version_info__ 22 | 23 | if sys.version_info < (2, 6): 24 | raise RuntimeError('You need Python 2.6+ for this module.') 25 | 26 | 27 | __author__ = "Jeff Forcier " 28 | __license__ = "GNU Lesser General Public License (LGPL)" 29 | 30 | 31 | from paramiko.transport import SecurityOptions, Transport 32 | from paramiko.client import ( 33 | SSHClient, MissingHostKeyPolicy, AutoAddPolicy, RejectPolicy, 34 | WarningPolicy, 35 | ) 36 | from paramiko.auth_handler import AuthHandler 37 | from paramiko.ssh_gss import GSSAuth, GSS_AUTH_AVAILABLE 38 | from paramiko.channel import Channel, ChannelFile 39 | from paramiko.ssh_exception import ( 40 | SSHException, PasswordRequiredException, BadAuthenticationType, 41 | ChannelException, BadHostKeyException, AuthenticationException, 42 | ProxyCommandFailure, 43 | ) 44 | from paramiko.server import ServerInterface, SubsystemHandler, InteractiveQuery 45 | from paramiko.rsakey import RSAKey 46 | from paramiko.dsskey import DSSKey 47 | from paramiko.ecdsakey import ECDSAKey 48 | from paramiko.ed25519key import Ed25519Key 49 | from paramiko.sftp import SFTPError, BaseSFTP 50 | from paramiko.sftp_client import SFTP, SFTPClient 51 | from paramiko.sftp_server import SFTPServer 52 | from paramiko.sftp_attr import SFTPAttributes 53 | from paramiko.sftp_handle import SFTPHandle 54 | from paramiko.sftp_si import SFTPServerInterface 55 | from paramiko.sftp_file import SFTPFile 56 | from paramiko.message import Message 57 | from paramiko.packet import Packetizer 58 | from paramiko.file import BufferedFile 59 | from paramiko.agent import Agent, AgentKey 60 | from paramiko.pkey import PKey 61 | from paramiko.hostkeys import HostKeys 62 | from paramiko.config import SSHConfig 63 | from paramiko.proxy import ProxyCommand 64 | 65 | from paramiko.common import ( 66 | AUTH_SUCCESSFUL, AUTH_PARTIALLY_SUCCESSFUL, AUTH_FAILED, OPEN_SUCCEEDED, 67 | OPEN_FAILED_ADMINISTRATIVELY_PROHIBITED, OPEN_FAILED_CONNECT_FAILED, 68 | OPEN_FAILED_UNKNOWN_CHANNEL_TYPE, OPEN_FAILED_RESOURCE_SHORTAGE, 69 | ) 70 | 71 | from paramiko.sftp import ( 72 | SFTP_OK, SFTP_EOF, SFTP_NO_SUCH_FILE, SFTP_PERMISSION_DENIED, SFTP_FAILURE, 73 | SFTP_BAD_MESSAGE, SFTP_NO_CONNECTION, SFTP_CONNECTION_LOST, 74 | SFTP_OP_UNSUPPORTED, 75 | ) 76 | 77 | from paramiko.common import io_sleep 78 | 79 | __all__ = [ 80 | 'Transport', 81 | 'SSHClient', 82 | 'MissingHostKeyPolicy', 83 | 'AutoAddPolicy', 84 | 'RejectPolicy', 85 | 'WarningPolicy', 86 | 'SecurityOptions', 87 | 'SubsystemHandler', 88 | 'Channel', 89 | 'PKey', 90 | 'RSAKey', 91 | 'DSSKey', 92 | 'Message', 93 | 'SSHException', 94 | 'AuthenticationException', 95 | 'PasswordRequiredException', 96 | 'BadAuthenticationType', 97 | 'ChannelException', 98 | 'BadHostKeyException', 99 | 'ProxyCommand', 100 | 'ProxyCommandFailure', 101 | 'SFTP', 102 | 'SFTPFile', 103 | 'SFTPHandle', 104 | 'SFTPClient', 105 | 'SFTPServer', 106 | 'SFTPError', 107 | 'SFTPAttributes', 108 | 'SFTPServerInterface', 109 | 'ServerInterface', 110 | 'BufferedFile', 111 | 'Agent', 112 | 'AgentKey', 113 | 'HostKeys', 114 | 'SSHConfig', 115 | 'util', 116 | 'io_sleep', 117 | ] 118 | -------------------------------------------------------------------------------- /wetland/services/exec_service.py: -------------------------------------------------------------------------------- 1 | import re 2 | import os 3 | import uuid 4 | import select 5 | from wetland.config import cfg 6 | 7 | 8 | def exec_service(hacker_session, docker_session, cmd, output): 9 | 10 | hacker_fileno = hacker_session.fileno() 11 | docker_fileno = docker_session.fileno() 12 | 13 | epoll = select.epoll() 14 | epoll.register(hacker_fileno, select.EPOLLIN) 15 | epoll.register(docker_fileno, select.EPOLLIN) 16 | 17 | docker_session.exec_command(cmd) 18 | output.o('content', 'exec', "N"*20) 19 | output.o('content', 'exec', '[H]:'+cmd.encode("hex")) 20 | output.o('wetland', 'exec command', cmd) 21 | 22 | filelen = 0 23 | nowlen = 0 24 | if re.match('^scp\s*(-r)?\s*-[t]\s*\S*$', cmd): 25 | isscp = True 26 | else: 27 | isscp = False 28 | 29 | try: 30 | while True: 31 | events = epoll.poll() 32 | 33 | for fd, event in events: 34 | if fd == hacker_fileno: 35 | 36 | if event & select.EPOLLIN: 37 | if hacker_session.recv_ready(): 38 | text = hacker_session.recv(1024) 39 | 40 | if not isscp: 41 | output.o('content', 'exec', '[H]:'+text.encode("hex")) 42 | output.o('wetland', 'exec command', text) 43 | else: 44 | if re.match('^C\d{4}\s+\d+\s+\S+\n$', text): 45 | filename = str(uuid.uuid1()).replace('-', '') 46 | filepath = os.path.join(cfg.get('files', 'path'), 47 | filename) 48 | scpfile = open(filepath, 'wb') 49 | filelen = int(text.split(' ')[1]) 50 | else: 51 | if nowlen >= filelen: 52 | pass 53 | else: 54 | scpfile.write(text) 55 | nowlen += len(text) 56 | if nowlen >= filelen: 57 | scpfile.close() 58 | nowlen = 0 59 | filelen = 0 60 | # output.o('upfile', 'scp', filename) 61 | # output.upfile(filename) 62 | 63 | # print 'hacker said: ', text.encode("hex"), text 64 | docker_session.sendall(text) 65 | if hacker_session.eof_received: 66 | docker_session.shutdown_write() 67 | docker_session.send_exit_status(0) 68 | 69 | elif event & select.EPOLLHUP: 70 | break 71 | 72 | elif fd == docker_fileno: 73 | if event & select.EPOLLIN: 74 | 75 | if docker_session.recv_ready(): 76 | text = docker_session.recv(1024) 77 | output.o('content', 'exec', 78 | '[V]:'+text.encode("hex")) 79 | # print 'docker said: ', text.encode("hex"), text 80 | hacker_session.sendall(text) 81 | 82 | if docker_session.recv_stderr_ready(): 83 | text = docker_session.recv_stderr(1024) 84 | hacker_session.sendall_stderr(text) 85 | 86 | if docker_session.eof_received: 87 | hacker_session.shutdown_write() 88 | hacker_session.send_exit_status(0) 89 | 90 | elif event & select.EPOLLHUP: 91 | break 92 | 93 | if docker_session.eof_received or hacker_session.eof_received: 94 | break 95 | 96 | except Exception, e: 97 | print e 98 | finally: 99 | docker_session.close() 100 | hacker_session.close() 101 | -------------------------------------------------------------------------------- /paramiko/pipe.py: -------------------------------------------------------------------------------- 1 | # Copyright (C) 2003-2007 Robey Pointer 2 | # 3 | # This file is part of paramiko. 4 | # 5 | # Paramiko is free software; you can redistribute it and/or modify it under the 6 | # terms of the GNU Lesser General Public License as published by the Free 7 | # Software Foundation; either version 2.1 of the License, or (at your option) 8 | # any later version. 9 | # 10 | # Paramiko is distributed in the hope that it will be useful, but WITHOUT ANY 11 | # WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR 12 | # A PARTICULAR PURPOSE. See the GNU Lesser General Public License for more 13 | # details. 14 | # 15 | # You should have received a copy of the GNU Lesser General Public License 16 | # along with Paramiko; if not, write to the Free Software Foundation, Inc., 17 | # 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA. 18 | 19 | """ 20 | Abstraction of a one-way pipe where the read end can be used in 21 | `select.select`. Normally this is trivial, but Windows makes it nearly 22 | impossible. 23 | 24 | The pipe acts like an Event, which can be set or cleared. When set, the pipe 25 | will trigger as readable in `select `. 26 | """ 27 | 28 | import sys 29 | import os 30 | import socket 31 | 32 | 33 | def make_pipe(): 34 | if sys.platform[:3] != 'win': 35 | p = PosixPipe() 36 | else: 37 | p = WindowsPipe() 38 | return p 39 | 40 | 41 | class PosixPipe (object): 42 | def __init__(self): 43 | self._rfd, self._wfd = os.pipe() 44 | self._set = False 45 | self._forever = False 46 | self._closed = False 47 | 48 | def close(self): 49 | os.close(self._rfd) 50 | os.close(self._wfd) 51 | # used for unit tests: 52 | self._closed = True 53 | 54 | def fileno(self): 55 | return self._rfd 56 | 57 | def clear(self): 58 | if not self._set or self._forever: 59 | return 60 | os.read(self._rfd, 1) 61 | self._set = False 62 | 63 | def set(self): 64 | if self._set or self._closed: 65 | return 66 | self._set = True 67 | os.write(self._wfd, b'*') 68 | 69 | def set_forever(self): 70 | self._forever = True 71 | self.set() 72 | 73 | 74 | class WindowsPipe (object): 75 | """ 76 | On Windows, only an OS-level "WinSock" may be used in select(), but reads 77 | and writes must be to the actual socket object. 78 | """ 79 | def __init__(self): 80 | serv = socket.socket(socket.AF_INET, socket.SOCK_STREAM) 81 | serv.bind(('127.0.0.1', 0)) 82 | serv.listen(1) 83 | 84 | # need to save sockets in _rsock/_wsock so they don't get closed 85 | self._rsock = socket.socket(socket.AF_INET, socket.SOCK_STREAM) 86 | self._rsock.connect(('127.0.0.1', serv.getsockname()[1])) 87 | 88 | self._wsock, addr = serv.accept() 89 | serv.close() 90 | self._set = False 91 | self._forever = False 92 | self._closed = False 93 | 94 | def close(self): 95 | self._rsock.close() 96 | self._wsock.close() 97 | # used for unit tests: 98 | self._closed = True 99 | 100 | def fileno(self): 101 | return self._rsock.fileno() 102 | 103 | def clear(self): 104 | if not self._set or self._forever: 105 | return 106 | self._rsock.recv(1) 107 | self._set = False 108 | 109 | def set(self): 110 | if self._set or self._closed: 111 | return 112 | self._set = True 113 | self._wsock.send(b'*') 114 | 115 | def set_forever(self): 116 | self._forever = True 117 | self.set() 118 | 119 | 120 | class OrPipe (object): 121 | def __init__(self, pipe): 122 | self._set = False 123 | self._partner = None 124 | self._pipe = pipe 125 | 126 | def set(self): 127 | self._set = True 128 | if not self._partner._set: 129 | self._pipe.set() 130 | 131 | def clear(self): 132 | self._set = False 133 | if not self._partner._set: 134 | self._pipe.clear() 135 | 136 | 137 | def make_or_pipe(pipe): 138 | """ 139 | wraps a pipe into two pipe-like objects which are "or"d together to 140 | affect the real pipe. if either returned pipe is set, the wrapped pipe 141 | is set. when both are cleared, the wrapped pipe is cleared. 142 | """ 143 | p1 = OrPipe(pipe) 144 | p2 = OrPipe(pipe) 145 | p1._partner = p2 146 | p2._partner = p1 147 | return p1, p2 148 | -------------------------------------------------------------------------------- /paramiko/py3compat.py: -------------------------------------------------------------------------------- 1 | import sys 2 | import base64 3 | 4 | __all__ = ['PY2', 'string_types', 'integer_types', 'text_type', 'bytes_types', 5 | 'bytes', 'long', 'input', 'decodebytes', 'encodebytes', 6 | 'bytestring', 'byte_ord', 'byte_chr', 'byte_mask', 'b', 'u', 'b2s', 7 | 'StringIO', 'BytesIO', 'is_callable', 'MAXSIZE', 8 | 'next', 'builtins'] 9 | 10 | PY2 = sys.version_info[0] < 3 11 | 12 | if PY2: 13 | string_types = basestring # NOQA 14 | text_type = unicode # NOQA 15 | bytes_types = str 16 | bytes = str 17 | integer_types = (int, long) # NOQA 18 | long = long # NOQA 19 | input = raw_input # NOQA 20 | decodebytes = base64.decodestring 21 | encodebytes = base64.encodestring 22 | 23 | import __builtin__ as builtins 24 | 25 | 26 | def bytestring(s): # NOQA 27 | if isinstance(s, unicode): # NOQA 28 | return s.encode('utf-8') 29 | return s 30 | 31 | 32 | byte_ord = ord # NOQA 33 | byte_chr = chr # NOQA 34 | 35 | 36 | def byte_mask(c, mask): 37 | return chr(ord(c) & mask) 38 | 39 | 40 | def b(s, encoding='utf8'): # NOQA 41 | """cast unicode or bytes to bytes""" 42 | if isinstance(s, str): 43 | return s 44 | elif isinstance(s, unicode): # NOQA 45 | return s.encode(encoding) 46 | elif isinstance(s, buffer): # NOQA 47 | return s 48 | else: 49 | raise TypeError("Expected unicode or bytes, got %r" % s) 50 | 51 | 52 | def u(s, encoding='utf8'): # NOQA 53 | """cast bytes or unicode to unicode""" 54 | if isinstance(s, str): 55 | return s.decode(encoding) 56 | elif isinstance(s, unicode): # NOQA 57 | return s 58 | elif isinstance(s, buffer): # NOQA 59 | return s.decode(encoding) 60 | else: 61 | raise TypeError("Expected unicode or bytes, got %r" % s) 62 | 63 | 64 | def b2s(s): 65 | return s 66 | 67 | 68 | import cStringIO 69 | StringIO = cStringIO.StringIO 70 | BytesIO = StringIO 71 | 72 | 73 | def is_callable(c): # NOQA 74 | return callable(c) 75 | 76 | 77 | def get_next(c): # NOQA 78 | return c.next 79 | 80 | 81 | def next(c): 82 | return c.next() 83 | 84 | # It's possible to have sizeof(long) != sizeof(Py_ssize_t). 85 | class X(object): 86 | def __len__(self): 87 | return 1 << 31 88 | 89 | 90 | try: 91 | len(X()) 92 | except OverflowError: 93 | # 32-bit 94 | MAXSIZE = int((1 << 31) - 1) # NOQA 95 | else: 96 | # 64-bit 97 | MAXSIZE = int((1 << 63) - 1) # NOQA 98 | del X 99 | else: 100 | import collections 101 | import struct 102 | import builtins 103 | string_types = str 104 | text_type = str 105 | bytes = bytes 106 | bytes_types = bytes 107 | integer_types = int 108 | class long(int): 109 | pass 110 | input = input 111 | decodebytes = base64.decodebytes 112 | encodebytes = base64.encodebytes 113 | 114 | def bytestring(s): 115 | return s 116 | 117 | def byte_ord(c): 118 | # In case we're handed a string instead of an int. 119 | if not isinstance(c, int): 120 | c = ord(c) 121 | return c 122 | 123 | def byte_chr(c): 124 | assert isinstance(c, int) 125 | return struct.pack('B', c) 126 | 127 | def byte_mask(c, mask): 128 | assert isinstance(c, int) 129 | return struct.pack('B', c & mask) 130 | 131 | def b(s, encoding='utf8'): 132 | """cast unicode or bytes to bytes""" 133 | if isinstance(s, bytes): 134 | return s 135 | elif isinstance(s, str): 136 | return s.encode(encoding) 137 | else: 138 | raise TypeError("Expected unicode or bytes, got %r" % s) 139 | 140 | def u(s, encoding='utf8'): 141 | """cast bytes or unicode to unicode""" 142 | if isinstance(s, bytes): 143 | return s.decode(encoding) 144 | elif isinstance(s, str): 145 | return s 146 | else: 147 | raise TypeError("Expected unicode or bytes, got %r" % s) 148 | 149 | def b2s(s): 150 | return s.decode() if isinstance(s, bytes) else s 151 | 152 | import io 153 | StringIO = io.StringIO # NOQA 154 | BytesIO = io.BytesIO # NOQA 155 | 156 | def is_callable(c): 157 | return isinstance(c, collections.Callable) 158 | 159 | def get_next(c): 160 | return c.__next__ 161 | 162 | next = next 163 | 164 | MAXSIZE = sys.maxsize # NOQA 165 | -------------------------------------------------------------------------------- /paramiko/kex_ecdh_nist.py: -------------------------------------------------------------------------------- 1 | """ 2 | Ephemeral Elliptic Curve Diffie-Hellman (ECDH) key exchange 3 | RFC 5656, Section 4 4 | """ 5 | 6 | from hashlib import sha256, sha384, sha512 7 | from paramiko.message import Message 8 | from paramiko.py3compat import byte_chr, long 9 | from paramiko.ssh_exception import SSHException 10 | from cryptography.hazmat.backends import default_backend 11 | from cryptography.hazmat.primitives.asymmetric import ec 12 | from binascii import hexlify 13 | 14 | _MSG_KEXECDH_INIT, _MSG_KEXECDH_REPLY = range(30, 32) 15 | c_MSG_KEXECDH_INIT, c_MSG_KEXECDH_REPLY = [byte_chr(c) for c in range(30, 32)] 16 | 17 | 18 | class KexNistp256(): 19 | 20 | name = "ecdh-sha2-nistp256" 21 | hash_algo = sha256 22 | curve = ec.SECP256R1() 23 | 24 | def __init__(self, transport): 25 | self.transport = transport 26 | # private key, client public and server public keys 27 | self.P = long(0) 28 | self.Q_C = None 29 | self.Q_S = None 30 | 31 | def start_kex(self): 32 | self._generate_key_pair() 33 | if self.transport.server_mode: 34 | self.transport._expect_packet(_MSG_KEXECDH_INIT) 35 | return 36 | m = Message() 37 | m.add_byte(c_MSG_KEXECDH_INIT) 38 | # SEC1: V2.0 2.3.3 Elliptic-Curve-Point-to-Octet-String Conversion 39 | m.add_string(self.Q_C.public_numbers().encode_point()) 40 | self.transport._send_message(m) 41 | self.transport._expect_packet(_MSG_KEXECDH_REPLY) 42 | 43 | def parse_next(self, ptype, m): 44 | if self.transport.server_mode and (ptype == _MSG_KEXECDH_INIT): 45 | return self._parse_kexecdh_init(m) 46 | elif not self.transport.server_mode and (ptype == _MSG_KEXECDH_REPLY): 47 | return self._parse_kexecdh_reply(m) 48 | raise SSHException('KexECDH asked to handle packet type %d' % ptype) 49 | 50 | def _generate_key_pair(self): 51 | self.P = ec.generate_private_key(self.curve, default_backend()) 52 | if self.transport.server_mode: 53 | self.Q_S = self.P.public_key() 54 | return 55 | self.Q_C = self.P.public_key() 56 | 57 | def _parse_kexecdh_init(self, m): 58 | Q_C_bytes = m.get_string() 59 | self.Q_C = ec.EllipticCurvePublicNumbers.from_encoded_point( 60 | self.curve, Q_C_bytes 61 | ) 62 | K_S = self.transport.get_server_key().asbytes() 63 | K = self.P.exchange(ec.ECDH(), self.Q_C.public_key(default_backend())) 64 | K = long(hexlify(K), 16) 65 | # compute exchange hash 66 | hm = Message() 67 | hm.add(self.transport.remote_version, self.transport.local_version, 68 | self.transport.remote_kex_init, self.transport.local_kex_init) 69 | hm.add_string(K_S) 70 | hm.add_string(Q_C_bytes) 71 | # SEC1: V2.0 2.3.3 Elliptic-Curve-Point-to-Octet-String Conversion 72 | hm.add_string(self.Q_S.public_numbers().encode_point()) 73 | hm.add_mpint(long(K)) 74 | H = self.hash_algo(hm.asbytes()).digest() 75 | self.transport._set_K_H(K, H) 76 | sig = self.transport.get_server_key().sign_ssh_data(H) 77 | # construct reply 78 | m = Message() 79 | m.add_byte(c_MSG_KEXECDH_REPLY) 80 | m.add_string(K_S) 81 | m.add_string(self.Q_S.public_numbers().encode_point()) 82 | m.add_string(sig) 83 | self.transport._send_message(m) 84 | self.transport._activate_outbound() 85 | 86 | def _parse_kexecdh_reply(self, m): 87 | K_S = m.get_string() 88 | Q_S_bytes = m.get_string() 89 | self.Q_S = ec.EllipticCurvePublicNumbers.from_encoded_point( 90 | self.curve, Q_S_bytes 91 | ) 92 | sig = m.get_binary() 93 | K = self.P.exchange(ec.ECDH(), self.Q_S.public_key(default_backend())) 94 | K = long(hexlify(K), 16) 95 | # compute exchange hash and verify signature 96 | hm = Message() 97 | hm.add(self.transport.local_version, self.transport.remote_version, 98 | self.transport.local_kex_init, self.transport.remote_kex_init) 99 | hm.add_string(K_S) 100 | # SEC1: V2.0 2.3.3 Elliptic-Curve-Point-to-Octet-String Conversion 101 | hm.add_string(self.Q_C.public_numbers().encode_point()) 102 | hm.add_string(Q_S_bytes) 103 | hm.add_mpint(K) 104 | self.transport._set_K_H(K, self.hash_algo(hm.asbytes()).digest()) 105 | self.transport._verify_key(K_S, sig) 106 | self.transport._activate_outbound() 107 | 108 | 109 | class KexNistp384(KexNistp256): 110 | name = "ecdh-sha2-nistp384" 111 | hash_algo = sha384 112 | curve = ec.SECP384R1() 113 | 114 | 115 | class KexNistp521(KexNistp256): 116 | name = "ecdh-sha2-nistp521" 117 | hash_algo = sha512 118 | curve = ec.SECP521R1() 119 | -------------------------------------------------------------------------------- /paramiko/win_pageant.py: -------------------------------------------------------------------------------- 1 | # Copyright (C) 2005 John Arbash-Meinel 2 | # Modified up by: Todd Whiteman 3 | # 4 | # This file is part of paramiko. 5 | # 6 | # Paramiko is free software; you can redistribute it and/or modify it under the 7 | # terms of the GNU Lesser General Public License as published by the Free 8 | # Software Foundation; either version 2.1 of the License, or (at your option) 9 | # any later version. 10 | # 11 | # Paramiko is distributed in the hope that it will be useful, but WITHOUT ANY 12 | # WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR 13 | # A PARTICULAR PURPOSE. See the GNU Lesser General Public License for more 14 | # details. 15 | # 16 | # You should have received a copy of the GNU Lesser General Public License 17 | # along with Paramiko; if not, write to the Free Software Foundation, Inc., 18 | # 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA. 19 | 20 | """ 21 | Functions for communicating with Pageant, the basic windows ssh agent program. 22 | """ 23 | 24 | import array 25 | import ctypes.wintypes 26 | import platform 27 | import struct 28 | from paramiko.common import zero_byte 29 | from paramiko.py3compat import b 30 | 31 | try: 32 | import _thread as thread # Python 3.x 33 | except ImportError: 34 | import thread # Python 2.5-2.7 35 | 36 | from . import _winapi 37 | 38 | 39 | _AGENT_COPYDATA_ID = 0x804e50ba 40 | _AGENT_MAX_MSGLEN = 8192 41 | # Note: The WM_COPYDATA value is pulled from win32con, as a workaround 42 | # so we do not have to import this huge library just for this one variable. 43 | win32con_WM_COPYDATA = 74 44 | 45 | 46 | def _get_pageant_window_object(): 47 | return ctypes.windll.user32.FindWindowA(b('Pageant'), b('Pageant')) 48 | 49 | 50 | def can_talk_to_agent(): 51 | """ 52 | Check to see if there is a "Pageant" agent we can talk to. 53 | 54 | This checks both if we have the required libraries (win32all or ctypes) 55 | and if there is a Pageant currently running. 56 | """ 57 | return bool(_get_pageant_window_object()) 58 | 59 | 60 | if platform.architecture()[0] == '64bit': 61 | ULONG_PTR = ctypes.c_uint64 62 | else: 63 | ULONG_PTR = ctypes.c_uint32 64 | 65 | 66 | class COPYDATASTRUCT(ctypes.Structure): 67 | """ 68 | ctypes implementation of 69 | http://msdn.microsoft.com/en-us/library/windows/desktop/ms649010%28v=vs.85%29.aspx 70 | """ 71 | _fields_ = [ 72 | ('num_data', ULONG_PTR), 73 | ('data_size', ctypes.wintypes.DWORD), 74 | ('data_loc', ctypes.c_void_p), 75 | ] 76 | 77 | 78 | def _query_pageant(msg): 79 | """ 80 | Communication with the Pageant process is done through a shared 81 | memory-mapped file. 82 | """ 83 | hwnd = _get_pageant_window_object() 84 | if not hwnd: 85 | # Raise a failure to connect exception, pageant isn't running anymore! 86 | return None 87 | 88 | # create a name for the mmap 89 | map_name = 'PageantRequest%08x' % thread.get_ident() 90 | 91 | pymap = _winapi.MemoryMap(map_name, _AGENT_MAX_MSGLEN, 92 | _winapi.get_security_attributes_for_user(), 93 | ) 94 | with pymap: 95 | pymap.write(msg) 96 | # Create an array buffer containing the mapped filename 97 | char_buffer = array.array("b", b(map_name) + zero_byte) # noqa 98 | char_buffer_address, char_buffer_size = char_buffer.buffer_info() 99 | # Create a string to use for the SendMessage function call 100 | cds = COPYDATASTRUCT(_AGENT_COPYDATA_ID, char_buffer_size, 101 | char_buffer_address) 102 | 103 | response = ctypes.windll.user32.SendMessageA(hwnd, 104 | win32con_WM_COPYDATA, ctypes.sizeof(cds), ctypes.byref(cds)) 105 | 106 | if response > 0: 107 | pymap.seek(0) 108 | datalen = pymap.read(4) 109 | retlen = struct.unpack('>I', datalen)[0] 110 | return datalen + pymap.read(retlen) 111 | return None 112 | 113 | 114 | class PageantConnection(object): 115 | """ 116 | Mock "connection" to an agent which roughly approximates the behavior of 117 | a unix local-domain socket (as used by Agent). Requests are sent to the 118 | pageant daemon via special Windows magick, and responses are buffered back 119 | for subsequent reads. 120 | """ 121 | 122 | def __init__(self): 123 | self._response = None 124 | 125 | def send(self, data): 126 | self._response = _query_pageant(data) 127 | 128 | def recv(self, n): 129 | if self._response is None: 130 | return '' 131 | ret = self._response[:n] 132 | self._response = self._response[n:] 133 | if self._response == '': 134 | self._response = None 135 | return ret 136 | 137 | def close(self): 138 | pass 139 | -------------------------------------------------------------------------------- /paramiko/ber.py: -------------------------------------------------------------------------------- 1 | # Copyright (C) 2003-2007 Robey Pointer 2 | # 3 | # This file is part of paramiko. 4 | # 5 | # Paramiko is free software; you can redistribute it and/or modify it under the 6 | # terms of the GNU Lesser General Public License as published by the Free 7 | # Software Foundation; either version 2.1 of the License, or (at your option) 8 | # any later version. 9 | # 10 | # Paramiko is distributed in the hope that it will be useful, but WITHOUT ANY 11 | # WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR 12 | # A PARTICULAR PURPOSE. See the GNU Lesser General Public License for more 13 | # details. 14 | # 15 | # You should have received a copy of the GNU Lesser General Public License 16 | # along with Paramiko; if not, write to the Free Software Foundation, Inc., 17 | # 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA. 18 | from paramiko.common import max_byte, zero_byte 19 | from paramiko.py3compat import b, byte_ord, byte_chr, long 20 | 21 | import paramiko.util as util 22 | 23 | 24 | class BERException (Exception): 25 | pass 26 | 27 | 28 | class BER(object): 29 | """ 30 | Robey's tiny little attempt at a BER decoder. 31 | """ 32 | 33 | def __init__(self, content=bytes()): 34 | self.content = b(content) 35 | self.idx = 0 36 | 37 | def asbytes(self): 38 | return self.content 39 | 40 | def __str__(self): 41 | return self.asbytes() 42 | 43 | def __repr__(self): 44 | return 'BER(\'' + repr(self.content) + '\')' 45 | 46 | def decode(self): 47 | return self.decode_next() 48 | 49 | def decode_next(self): 50 | if self.idx >= len(self.content): 51 | return None 52 | ident = byte_ord(self.content[self.idx]) 53 | self.idx += 1 54 | if (ident & 31) == 31: 55 | # identifier > 30 56 | ident = 0 57 | while self.idx < len(self.content): 58 | t = byte_ord(self.content[self.idx]) 59 | self.idx += 1 60 | ident = (ident << 7) | (t & 0x7f) 61 | if not (t & 0x80): 62 | break 63 | if self.idx >= len(self.content): 64 | return None 65 | # now fetch length 66 | size = byte_ord(self.content[self.idx]) 67 | self.idx += 1 68 | if size & 0x80: 69 | # more complimicated... 70 | # FIXME: theoretically should handle indefinite-length (0x80) 71 | t = size & 0x7f 72 | if self.idx + t > len(self.content): 73 | return None 74 | size = util.inflate_long( 75 | self.content[self.idx: self.idx + t], True) 76 | self.idx += t 77 | if self.idx + size > len(self.content): 78 | # can't fit 79 | return None 80 | data = self.content[self.idx: self.idx + size] 81 | self.idx += size 82 | # now switch on id 83 | if ident == 0x30: 84 | # sequence 85 | return self.decode_sequence(data) 86 | elif ident == 2: 87 | # int 88 | return util.inflate_long(data) 89 | else: 90 | # 1: boolean (00 false, otherwise true) 91 | raise BERException( 92 | 'Unknown ber encoding type %d (robey is lazy)' % ident) 93 | 94 | @staticmethod 95 | def decode_sequence(data): 96 | out = [] 97 | ber = BER(data) 98 | while True: 99 | x = ber.decode_next() 100 | if x is None: 101 | break 102 | out.append(x) 103 | return out 104 | 105 | def encode_tlv(self, ident, val): 106 | # no need to support ident > 31 here 107 | self.content += byte_chr(ident) 108 | if len(val) > 0x7f: 109 | lenstr = util.deflate_long(len(val)) 110 | self.content += byte_chr(0x80 + len(lenstr)) + lenstr 111 | else: 112 | self.content += byte_chr(len(val)) 113 | self.content += val 114 | 115 | def encode(self, x): 116 | if type(x) is bool: 117 | if x: 118 | self.encode_tlv(1, max_byte) 119 | else: 120 | self.encode_tlv(1, zero_byte) 121 | elif (type(x) is int) or (type(x) is long): 122 | self.encode_tlv(2, util.deflate_long(x)) 123 | elif type(x) is str: 124 | self.encode_tlv(4, x) 125 | elif (type(x) is list) or (type(x) is tuple): 126 | self.encode_tlv(0x30, self.encode_sequence(x)) 127 | else: 128 | raise BERException('Unknown type for encoding: %s' % repr(type(x))) 129 | 130 | @staticmethod 131 | def encode_sequence(data): 132 | ber = BER() 133 | for item in data: 134 | ber.encode(item) 135 | return ber.asbytes() 136 | -------------------------------------------------------------------------------- /paramiko/proxy.py: -------------------------------------------------------------------------------- 1 | # Copyright (C) 2012 Yipit, Inc 2 | # 3 | # This file is part of paramiko. 4 | # 5 | # Paramiko is free software; you can redistribute it and/or modify it under the 6 | # terms of the GNU Lesser General Public License as published by the Free 7 | # Software Foundation; either version 2.1 of the License, or (at your option) 8 | # any later version. 9 | # 10 | # Paramiko is distrubuted in the hope that it will be useful, but WITHOUT ANY 11 | # WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR 12 | # A PARTICULAR PURPOSE. See the GNU Lesser General Public License for more 13 | # details. 14 | # 15 | # You should have received a copy of the GNU Lesser General Public License 16 | # along with Paramiko; if not, write to the Free Software Foundation, Inc., 17 | # 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA. 18 | 19 | 20 | import os 21 | from shlex import split as shlsplit 22 | import signal 23 | from select import select 24 | import socket 25 | import time 26 | 27 | from paramiko.ssh_exception import ProxyCommandFailure 28 | from paramiko.util import ClosingContextManager 29 | 30 | 31 | class ProxyCommand(ClosingContextManager): 32 | """ 33 | Wraps a subprocess running ProxyCommand-driven programs. 34 | 35 | This class implements a the socket-like interface needed by the 36 | `.Transport` and `.Packetizer` classes. Using this class instead of a 37 | regular socket makes it possible to talk with a Popen'd command that will 38 | proxy traffic between the client and a server hosted in another machine. 39 | 40 | Instances of this class may be used as context managers. 41 | """ 42 | def __init__(self, command_line): 43 | """ 44 | Create a new CommandProxy instance. The instance created by this 45 | class can be passed as an argument to the `.Transport` class. 46 | 47 | :param str command_line: 48 | the command that should be executed and used as the proxy. 49 | """ 50 | # NOTE: subprocess import done lazily so platforms without it (e.g. 51 | # GAE) can still import us during overall Paramiko load. 52 | from subprocess import Popen, PIPE 53 | self.cmd = shlsplit(command_line) 54 | self.process = Popen(self.cmd, stdin=PIPE, stdout=PIPE, stderr=PIPE, 55 | bufsize=0) 56 | self.timeout = None 57 | 58 | def send(self, content): 59 | """ 60 | Write the content received from the SSH client to the standard 61 | input of the forked command. 62 | 63 | :param str content: string to be sent to the forked command 64 | """ 65 | try: 66 | self.process.stdin.write(content) 67 | except IOError as e: 68 | # There was a problem with the child process. It probably 69 | # died and we can't proceed. The best option here is to 70 | # raise an exception informing the user that the informed 71 | # ProxyCommand is not working. 72 | raise ProxyCommandFailure(' '.join(self.cmd), e.strerror) 73 | return len(content) 74 | 75 | def recv(self, size): 76 | """ 77 | Read from the standard output of the forked program. 78 | 79 | :param int size: how many chars should be read 80 | 81 | :return: the string of bytes read, which may be shorter than requested 82 | """ 83 | try: 84 | buffer = b'' 85 | start = time.time() 86 | while len(buffer) < size: 87 | select_timeout = None 88 | if self.timeout is not None: 89 | elapsed = (time.time() - start) 90 | if elapsed >= self.timeout: 91 | raise socket.timeout() 92 | select_timeout = self.timeout - elapsed 93 | 94 | r, w, x = select( 95 | [self.process.stdout], [], [], select_timeout) 96 | if r and r[0] == self.process.stdout: 97 | buffer += os.read( 98 | self.process.stdout.fileno(), size - len(buffer)) 99 | return buffer 100 | except socket.timeout: 101 | if buffer: 102 | # Don't raise socket.timeout, return partial result instead 103 | return buffer 104 | raise # socket.timeout is a subclass of IOError 105 | except IOError as e: 106 | raise ProxyCommandFailure(' '.join(self.cmd), e.strerror) 107 | 108 | def close(self): 109 | os.kill(self.process.pid, signal.SIGTERM) 110 | 111 | @property 112 | def closed(self): 113 | return self.process.returncode is not None 114 | 115 | @property 116 | def _closed(self): 117 | # Concession to Python 3 socket-like API 118 | return self.closed 119 | 120 | def settimeout(self, timeout): 121 | self.timeout = timeout 122 | -------------------------------------------------------------------------------- /paramiko/primes.py: -------------------------------------------------------------------------------- 1 | # Copyright (C) 2003-2007 Robey Pointer 2 | # 3 | # This file is part of paramiko. 4 | # 5 | # Paramiko is free software; you can redistribute it and/or modify it under the 6 | # terms of the GNU Lesser General Public License as published by the Free 7 | # Software Foundation; either version 2.1 of the License, or (at your option) 8 | # any later version. 9 | # 10 | # Paramiko is distributed in the hope that it will be useful, but WITHOUT ANY 11 | # WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR 12 | # A PARTICULAR PURPOSE. See the GNU Lesser General Public License for more 13 | # details. 14 | # 15 | # You should have received a copy of the GNU Lesser General Public License 16 | # along with Paramiko; if not, write to the Free Software Foundation, Inc., 17 | # 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA. 18 | 19 | """ 20 | Utility functions for dealing with primes. 21 | """ 22 | 23 | import os 24 | 25 | from paramiko import util 26 | from paramiko.py3compat import byte_mask, long 27 | from paramiko.ssh_exception import SSHException 28 | 29 | 30 | def _roll_random(n): 31 | """returns a random # from 0 to N-1""" 32 | bits = util.bit_length(n - 1) 33 | byte_count = (bits + 7) // 8 34 | hbyte_mask = pow(2, bits % 8) - 1 35 | 36 | # so here's the plan: 37 | # we fetch as many random bits as we'd need to fit N-1, and if the 38 | # generated number is >= N, we try again. in the worst case (N-1 is a 39 | # power of 2), we have slightly better than 50% odds of getting one that 40 | # fits, so i can't guarantee that this loop will ever finish, but the odds 41 | # of it looping forever should be infinitesimal. 42 | while True: 43 | x = os.urandom(byte_count) 44 | if hbyte_mask > 0: 45 | x = byte_mask(x[0], hbyte_mask) + x[1:] 46 | num = util.inflate_long(x, 1) 47 | if num < n: 48 | break 49 | return num 50 | 51 | 52 | class ModulusPack (object): 53 | """ 54 | convenience object for holding the contents of the /etc/ssh/moduli file, 55 | on systems that have such a file. 56 | """ 57 | 58 | def __init__(self): 59 | # pack is a hash of: bits -> [ (generator, modulus) ... ] 60 | self.pack = {} 61 | self.discarded = [] 62 | 63 | def _parse_modulus(self, line): 64 | timestamp, mod_type, tests, tries, size, generator, modulus = \ 65 | line.split() 66 | mod_type = int(mod_type) 67 | tests = int(tests) 68 | tries = int(tries) 69 | size = int(size) 70 | generator = int(generator) 71 | modulus = long(modulus, 16) 72 | 73 | # weed out primes that aren't at least: 74 | # type 2 (meets basic structural requirements) 75 | # test 4 (more than just a small-prime sieve) 76 | # tries < 100 if test & 4 (at least 100 tries of miller-rabin) 77 | if ( 78 | mod_type < 2 or 79 | tests < 4 or 80 | (tests & 4 and tests < 8 and tries < 100) 81 | ): 82 | self.discarded.append( 83 | (modulus, 'does not meet basic requirements')) 84 | return 85 | if generator == 0: 86 | generator = 2 87 | 88 | # there's a bug in the ssh "moduli" file (yeah, i know: shock! dismay! 89 | # call cnn!) where it understates the bit lengths of these primes by 1. 90 | # this is okay. 91 | bl = util.bit_length(modulus) 92 | if (bl != size) and (bl != size + 1): 93 | self.discarded.append( 94 | (modulus, 'incorrectly reported bit length %d' % size)) 95 | return 96 | if bl not in self.pack: 97 | self.pack[bl] = [] 98 | self.pack[bl].append((generator, modulus)) 99 | 100 | def read_file(self, filename): 101 | """ 102 | :raises IOError: passed from any file operations that fail. 103 | """ 104 | self.pack = {} 105 | with open(filename, 'r') as f: 106 | for line in f: 107 | line = line.strip() 108 | if (len(line) == 0) or (line[0] == '#'): 109 | continue 110 | try: 111 | self._parse_modulus(line) 112 | except: 113 | continue 114 | 115 | def get_modulus(self, min, prefer, max): 116 | bitsizes = sorted(self.pack.keys()) 117 | if len(bitsizes) == 0: 118 | raise SSHException('no moduli available') 119 | good = -1 120 | # find nearest bitsize >= preferred 121 | for b in bitsizes: 122 | if (b >= prefer) and (b <= max) and (b < good or good == -1): 123 | good = b 124 | # if that failed, find greatest bitsize >= min 125 | if good == -1: 126 | for b in bitsizes: 127 | if (b >= min) and (b <= max) and (b > good): 128 | good = b 129 | if good == -1: 130 | # their entire (min, max) range has no intersection with our range. 131 | # if their range is below ours, pick the smallest. otherwise pick 132 | # the largest. it'll be out of their range requirement either way, 133 | # but we'll be sending them the closest one we have. 134 | good = bitsizes[0] 135 | if min > good: 136 | good = bitsizes[-1] 137 | # now pick a random modulus of this bitsize 138 | n = _roll_random(len(self.pack[good])) 139 | return self.pack[good][n] 140 | -------------------------------------------------------------------------------- /paramiko/kex_group1.py: -------------------------------------------------------------------------------- 1 | # Copyright (C) 2003-2007 Robey Pointer 2 | # 3 | # This file is part of paramiko. 4 | # 5 | # Paramiko is free software; you can redistribute it and/or modify it under the 6 | # terms of the GNU Lesser General Public License as published by the Free 7 | # Software Foundation; either version 2.1 of the License, or (at your option) 8 | # any later version. 9 | # 10 | # Paramiko is distributed in the hope that it will be useful, but WITHOUT ANY 11 | # WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR 12 | # A PARTICULAR PURPOSE. See the GNU Lesser General Public License for more 13 | # details. 14 | # 15 | # You should have received a copy of the GNU Lesser General Public License 16 | # along with Paramiko; if not, write to the Free Software Foundation, Inc., 17 | # 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA. 18 | 19 | """ 20 | Standard SSH key exchange ("kex" if you wanna sound cool). Diffie-Hellman of 21 | 1024 bit key halves, using a known "p" prime and "g" generator. 22 | """ 23 | 24 | import os 25 | from hashlib import sha1 26 | 27 | from paramiko import util 28 | from paramiko.common import max_byte, zero_byte 29 | from paramiko.message import Message 30 | from paramiko.py3compat import byte_chr, long, byte_mask 31 | from paramiko.ssh_exception import SSHException 32 | 33 | 34 | _MSG_KEXDH_INIT, _MSG_KEXDH_REPLY = range(30, 32) 35 | c_MSG_KEXDH_INIT, c_MSG_KEXDH_REPLY = [byte_chr(c) for c in range(30, 32)] 36 | 37 | b7fffffffffffffff = byte_chr(0x7f) + max_byte * 7 38 | b0000000000000000 = zero_byte * 8 39 | 40 | 41 | class KexGroup1(object): 42 | 43 | # draft-ietf-secsh-transport-09.txt, page 17 44 | P = 0xFFFFFFFFFFFFFFFFC90FDAA22168C234C4C6628B80DC1CD129024E088A67CC74020BBEA63B139B22514A08798E3404DDEF9519B3CD3A431B302B0A6DF25F14374FE1356D6D51C245E485B576625E7EC6F44C42E9A637ED6B0BFF5CB6F406B7EDEE386BFB5A899FA5AE9F24117C4B1FE649286651ECE65381FFFFFFFFFFFFFFFF # noqa 45 | G = 2 46 | 47 | name = 'diffie-hellman-group1-sha1' 48 | hash_algo = sha1 49 | 50 | def __init__(self, transport): 51 | self.transport = transport 52 | self.x = long(0) 53 | self.e = long(0) 54 | self.f = long(0) 55 | 56 | def start_kex(self): 57 | self._generate_x() 58 | if self.transport.server_mode: 59 | # compute f = g^x mod p, but don't send it yet 60 | self.f = pow(self.G, self.x, self.P) 61 | self.transport._expect_packet(_MSG_KEXDH_INIT) 62 | return 63 | # compute e = g^x mod p (where g=2), and send it 64 | self.e = pow(self.G, self.x, self.P) 65 | m = Message() 66 | m.add_byte(c_MSG_KEXDH_INIT) 67 | m.add_mpint(self.e) 68 | self.transport._send_message(m) 69 | self.transport._expect_packet(_MSG_KEXDH_REPLY) 70 | 71 | def parse_next(self, ptype, m): 72 | if self.transport.server_mode and (ptype == _MSG_KEXDH_INIT): 73 | return self._parse_kexdh_init(m) 74 | elif not self.transport.server_mode and (ptype == _MSG_KEXDH_REPLY): 75 | return self._parse_kexdh_reply(m) 76 | raise SSHException('KexGroup1 asked to handle packet type %d' % ptype) 77 | 78 | # ...internals... 79 | 80 | def _generate_x(self): 81 | # generate an "x" (1 < x < q), where q is (p-1)/2. 82 | # p is a 128-byte (1024-bit) number, where the first 64 bits are 1. 83 | # therefore q can be approximated as a 2^1023. we drop the subset of 84 | # potential x where the first 63 bits are 1, because some of those 85 | # will be larger than q (but this is a tiny tiny subset of 86 | # potential x). 87 | while 1: 88 | x_bytes = os.urandom(128) 89 | x_bytes = byte_mask(x_bytes[0], 0x7f) + x_bytes[1:] 90 | if (x_bytes[:8] != b7fffffffffffffff and 91 | x_bytes[:8] != b0000000000000000): 92 | break 93 | self.x = util.inflate_long(x_bytes) 94 | 95 | def _parse_kexdh_reply(self, m): 96 | # client mode 97 | host_key = m.get_string() 98 | self.f = m.get_mpint() 99 | if (self.f < 1) or (self.f > self.P - 1): 100 | raise SSHException('Server kex "f" is out of range') 101 | sig = m.get_binary() 102 | K = pow(self.f, self.x, self.P) 103 | # okay, build up the hash H of 104 | # (V_C || V_S || I_C || I_S || K_S || e || f || K) 105 | hm = Message() 106 | hm.add(self.transport.local_version, self.transport.remote_version, 107 | self.transport.local_kex_init, self.transport.remote_kex_init) 108 | hm.add_string(host_key) 109 | hm.add_mpint(self.e) 110 | hm.add_mpint(self.f) 111 | hm.add_mpint(K) 112 | self.transport._set_K_H(K, sha1(hm.asbytes()).digest()) 113 | self.transport._verify_key(host_key, sig) 114 | self.transport._activate_outbound() 115 | 116 | def _parse_kexdh_init(self, m): 117 | # server mode 118 | self.e = m.get_mpint() 119 | if (self.e < 1) or (self.e > self.P - 1): 120 | raise SSHException('Client kex "e" is out of range') 121 | K = pow(self.e, self.x, self.P) 122 | key = self.transport.get_server_key().asbytes() 123 | # okay, build up the hash H of 124 | # (V_C || V_S || I_C || I_S || K_S || e || f || K) 125 | hm = Message() 126 | hm.add(self.transport.remote_version, self.transport.local_version, 127 | self.transport.remote_kex_init, self.transport.local_kex_init) 128 | hm.add_string(key) 129 | hm.add_mpint(self.e) 130 | hm.add_mpint(self.f) 131 | hm.add_mpint(K) 132 | H = sha1(hm.asbytes()).digest() 133 | self.transport._set_K_H(K, H) 134 | # sign it 135 | sig = self.transport.get_server_key().sign_ssh_data(H) 136 | # send reply 137 | m = Message() 138 | m.add_byte(c_MSG_KEXDH_REPLY) 139 | m.add_string(key) 140 | m.add_mpint(self.f) 141 | m.add_string(sig) 142 | self.transport._send_message(m) 143 | self.transport._activate_outbound() 144 | -------------------------------------------------------------------------------- /paramiko/sftp.py: -------------------------------------------------------------------------------- 1 | # Copyright (C) 2003-2007 Robey Pointer 2 | # 3 | # This file is part of paramiko. 4 | # 5 | # Paramiko is free software; you can redistribute it and/or modify it under the 6 | # terms of the GNU Lesser General Public License as published by the Free 7 | # Software Foundation; either version 2.1 of the License, or (at your option) 8 | # any later version. 9 | # 10 | # Paramiko is distributed in the hope that it will be useful, but WITHOUT ANY 11 | # WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR 12 | # A PARTICULAR PURPOSE. See the GNU Lesser General Public License for more 13 | # details. 14 | # 15 | # You should have received a copy of the GNU Lesser General Public License 16 | # along with Paramiko; if not, write to the Free Software Foundation, Inc., 17 | # 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA. 18 | 19 | import select 20 | import socket 21 | import struct 22 | 23 | from paramiko import util 24 | from paramiko.common import asbytes, DEBUG 25 | from paramiko.message import Message 26 | from paramiko.py3compat import byte_chr, byte_ord 27 | 28 | 29 | CMD_INIT, CMD_VERSION, CMD_OPEN, CMD_CLOSE, CMD_READ, CMD_WRITE, CMD_LSTAT, \ 30 | CMD_FSTAT, CMD_SETSTAT, CMD_FSETSTAT, CMD_OPENDIR, CMD_READDIR, \ 31 | CMD_REMOVE, CMD_MKDIR, CMD_RMDIR, CMD_REALPATH, CMD_STAT, CMD_RENAME, \ 32 | CMD_READLINK, CMD_SYMLINK = range(1, 21) 33 | CMD_STATUS, CMD_HANDLE, CMD_DATA, CMD_NAME, CMD_ATTRS = range(101, 106) 34 | CMD_EXTENDED, CMD_EXTENDED_REPLY = range(200, 202) 35 | 36 | SFTP_OK = 0 37 | SFTP_EOF, SFTP_NO_SUCH_FILE, SFTP_PERMISSION_DENIED, SFTP_FAILURE, \ 38 | SFTP_BAD_MESSAGE, SFTP_NO_CONNECTION, SFTP_CONNECTION_LOST, \ 39 | SFTP_OP_UNSUPPORTED = range(1, 9) 40 | 41 | SFTP_DESC = ['Success', 42 | 'End of file', 43 | 'No such file', 44 | 'Permission denied', 45 | 'Failure', 46 | 'Bad message', 47 | 'No connection', 48 | 'Connection lost', 49 | 'Operation unsupported'] 50 | 51 | SFTP_FLAG_READ = 0x1 52 | SFTP_FLAG_WRITE = 0x2 53 | SFTP_FLAG_APPEND = 0x4 54 | SFTP_FLAG_CREATE = 0x8 55 | SFTP_FLAG_TRUNC = 0x10 56 | SFTP_FLAG_EXCL = 0x20 57 | 58 | _VERSION = 3 59 | 60 | 61 | # for debugging 62 | CMD_NAMES = { 63 | CMD_INIT: 'init', 64 | CMD_VERSION: 'version', 65 | CMD_OPEN: 'open', 66 | CMD_CLOSE: 'close', 67 | CMD_READ: 'read', 68 | CMD_WRITE: 'write', 69 | CMD_LSTAT: 'lstat', 70 | CMD_FSTAT: 'fstat', 71 | CMD_SETSTAT: 'setstat', 72 | CMD_FSETSTAT: 'fsetstat', 73 | CMD_OPENDIR: 'opendir', 74 | CMD_READDIR: 'readdir', 75 | CMD_REMOVE: 'remove', 76 | CMD_MKDIR: 'mkdir', 77 | CMD_RMDIR: 'rmdir', 78 | CMD_REALPATH: 'realpath', 79 | CMD_STAT: 'stat', 80 | CMD_RENAME: 'rename', 81 | CMD_READLINK: 'readlink', 82 | CMD_SYMLINK: 'symlink', 83 | CMD_STATUS: 'status', 84 | CMD_HANDLE: 'handle', 85 | CMD_DATA: 'data', 86 | CMD_NAME: 'name', 87 | CMD_ATTRS: 'attrs', 88 | CMD_EXTENDED: 'extended', 89 | CMD_EXTENDED_REPLY: 'extended_reply' 90 | } 91 | 92 | 93 | class SFTPError (Exception): 94 | pass 95 | 96 | 97 | class BaseSFTP (object): 98 | def __init__(self): 99 | self.logger = util.get_logger('paramiko.sftp') 100 | self.sock = None 101 | self.ultra_debug = False 102 | 103 | # ...internals... 104 | 105 | def _send_version(self): 106 | self._send_packet(CMD_INIT, struct.pack('>I', _VERSION)) 107 | t, data = self._read_packet() 108 | if t != CMD_VERSION: 109 | raise SFTPError('Incompatible sftp protocol') 110 | version = struct.unpack('>I', data[:4])[0] 111 | # if version != _VERSION: 112 | # raise SFTPError('Incompatible sftp protocol') 113 | return version 114 | 115 | def _send_server_version(self): 116 | # winscp will freak out if the server sends version info before the 117 | # client finishes sending INIT. 118 | t, data = self._read_packet() 119 | if t != CMD_INIT: 120 | raise SFTPError('Incompatible sftp protocol') 121 | version = struct.unpack('>I', data[:4])[0] 122 | # advertise that we support "check-file" 123 | extension_pairs = ['check-file', 'md5,sha1'] 124 | msg = Message() 125 | msg.add_int(_VERSION) 126 | msg.add(*extension_pairs) 127 | self._send_packet(CMD_VERSION, msg) 128 | return version 129 | 130 | def _log(self, level, msg, *args): 131 | self.logger.log(level, msg, *args) 132 | 133 | def _write_all(self, out): 134 | while len(out) > 0: 135 | n = self.sock.send(out) 136 | if n <= 0: 137 | raise EOFError() 138 | if n == len(out): 139 | return 140 | out = out[n:] 141 | return 142 | 143 | def _read_all(self, n): 144 | out = bytes() 145 | while n > 0: 146 | if isinstance(self.sock, socket.socket): 147 | # sometimes sftp is used directly over a socket instead of 148 | # through a paramiko channel. in this case, check periodically 149 | # if the socket is closed. (for some reason, recv() won't ever 150 | # return or raise an exception, but calling select on a closed 151 | # socket will.) 152 | while True: 153 | read, write, err = select.select([self.sock], [], [], 0.1) 154 | if len(read) > 0: 155 | x = self.sock.recv(n) 156 | break 157 | else: 158 | x = self.sock.recv(n) 159 | 160 | if len(x) == 0: 161 | raise EOFError() 162 | out += x 163 | n -= len(x) 164 | return out 165 | 166 | def _send_packet(self, t, packet): 167 | packet = asbytes(packet) 168 | out = struct.pack('>I', len(packet) + 1) + byte_chr(t) + packet 169 | if self.ultra_debug: 170 | self._log(DEBUG, util.format_binary(out, 'OUT: ')) 171 | self._write_all(out) 172 | 173 | def _read_packet(self): 174 | x = self._read_all(4) 175 | # most sftp servers won't accept packets larger than about 32k, so 176 | # anything with the high byte set (> 16MB) is just garbage. 177 | if byte_ord(x[0]): 178 | raise SFTPError('Garbage packet received') 179 | size = struct.unpack('>I', x)[0] 180 | data = self._read_all(size) 181 | if self.ultra_debug: 182 | self._log(DEBUG, util.format_binary(data, 'IN: ')) 183 | if size > 0: 184 | t = byte_ord(data[0]) 185 | return t, data[1:] 186 | return 0, bytes() 187 | -------------------------------------------------------------------------------- /paramiko/ssh_exception.py: -------------------------------------------------------------------------------- 1 | # Copyright (C) 2003-2007 Robey Pointer 2 | # 3 | # This file is part of paramiko. 4 | # 5 | # Paramiko is free software; you can redistribute it and/or modify it under the 6 | # terms of the GNU Lesser General Public License as published by the Free 7 | # Software Foundation; either version 2.1 of the License, or (at your option) 8 | # any later version. 9 | # 10 | # Paramiko is distributed in the hope that it will be useful, but WITHOUT ANY 11 | # WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR 12 | # A PARTICULAR PURPOSE. See the GNU Lesser General Public License for more 13 | # details. 14 | # 15 | # You should have received a copy of the GNU Lesser General Public License 16 | # along with Paramiko; if not, write to the Free Software Foundation, Inc., 17 | # 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA. 18 | 19 | import socket 20 | 21 | 22 | class SSHException (Exception): 23 | """ 24 | Exception raised by failures in SSH2 protocol negotiation or logic errors. 25 | """ 26 | pass 27 | 28 | 29 | class AuthenticationException (SSHException): 30 | """ 31 | Exception raised when authentication failed for some reason. It may be 32 | possible to retry with different credentials. (Other classes specify more 33 | specific reasons.) 34 | 35 | .. versionadded:: 1.6 36 | """ 37 | pass 38 | 39 | 40 | class PasswordRequiredException (AuthenticationException): 41 | """ 42 | Exception raised when a password is needed to unlock a private key file. 43 | """ 44 | pass 45 | 46 | 47 | class BadAuthenticationType (AuthenticationException): 48 | """ 49 | Exception raised when an authentication type (like password) is used, but 50 | the server isn't allowing that type. (It may only allow public-key, for 51 | example.) 52 | 53 | .. versionadded:: 1.1 54 | """ 55 | #: list of allowed authentication types provided by the server (possible 56 | #: values are: ``"none"``, ``"password"``, and ``"publickey"``). 57 | allowed_types = [] 58 | 59 | def __init__(self, explanation, types): 60 | AuthenticationException.__init__(self, explanation) 61 | self.allowed_types = types 62 | # for unpickling 63 | self.args = (explanation, types, ) 64 | 65 | def __str__(self): 66 | return '{0} (allowed_types={1!r})'.format( 67 | SSHException.__str__(self), self.allowed_types 68 | ) 69 | 70 | 71 | class PartialAuthentication (AuthenticationException): 72 | """ 73 | An internal exception thrown in the case of partial authentication. 74 | """ 75 | allowed_types = [] 76 | 77 | def __init__(self, types): 78 | AuthenticationException.__init__(self, 'partial authentication') 79 | self.allowed_types = types 80 | # for unpickling 81 | self.args = (types, ) 82 | 83 | 84 | class ChannelException (SSHException): 85 | """ 86 | Exception raised when an attempt to open a new `.Channel` fails. 87 | 88 | :param int code: the error code returned by the server 89 | 90 | .. versionadded:: 1.6 91 | """ 92 | def __init__(self, code, text): 93 | SSHException.__init__(self, text) 94 | self.code = code 95 | # for unpickling 96 | self.args = (code, text, ) 97 | 98 | 99 | class BadHostKeyException (SSHException): 100 | """ 101 | The host key given by the SSH server did not match what we were expecting. 102 | 103 | :param str hostname: the hostname of the SSH server 104 | :param PKey got_key: the host key presented by the server 105 | :param PKey expected_key: the host key expected 106 | 107 | .. versionadded:: 1.6 108 | """ 109 | def __init__(self, hostname, got_key, expected_key): 110 | message = 'Host key for server {0} does not match: got {1}, expected {2}' # noqa 111 | message = message.format( 112 | hostname, got_key.get_base64(), 113 | expected_key.get_base64()) 114 | SSHException.__init__(self, message) 115 | self.hostname = hostname 116 | self.key = got_key 117 | self.expected_key = expected_key 118 | # for unpickling 119 | self.args = (hostname, got_key, expected_key, ) 120 | 121 | 122 | class ProxyCommandFailure (SSHException): 123 | """ 124 | The "ProxyCommand" found in the .ssh/config file returned an error. 125 | 126 | :param str command: The command line that is generating this exception. 127 | :param str error: The error captured from the proxy command output. 128 | """ 129 | def __init__(self, command, error): 130 | SSHException.__init__(self, 131 | '"ProxyCommand (%s)" returned non-zero exit status: %s' % ( 132 | command, error 133 | ) 134 | ) 135 | self.error = error 136 | # for unpickling 137 | self.args = (command, error, ) 138 | 139 | 140 | class NoValidConnectionsError(socket.error): 141 | """ 142 | Multiple connection attempts were made and no families succeeded. 143 | 144 | This exception class wraps multiple "real" underlying connection errors, 145 | all of which represent failed connection attempts. Because these errors are 146 | not guaranteed to all be of the same error type (i.e. different errno, 147 | `socket.error` subclass, message, etc) we expose a single unified error 148 | message and a ``None`` errno so that instances of this class match most 149 | normal handling of `socket.error` objects. 150 | 151 | To see the wrapped exception objects, access the ``errors`` attribute. 152 | ``errors`` is a dict whose keys are address tuples (e.g. ``('127.0.0.1', 153 | 22)``) and whose values are the exception encountered trying to connect to 154 | that address. 155 | 156 | It is implied/assumed that all the errors given to a single instance of 157 | this class are from connecting to the same hostname + port (and thus that 158 | the differences are in the resolution of the hostname - e.g. IPv4 vs v6). 159 | 160 | .. versionadded:: 1.16 161 | """ 162 | def __init__(self, errors): 163 | """ 164 | :param dict errors: 165 | The errors dict to store, as described by class docstring. 166 | """ 167 | addrs = sorted(errors.keys()) 168 | body = ', '.join([x[0] for x in addrs[:-1]]) 169 | tail = addrs[-1][0] 170 | if body: 171 | msg = "Unable to connect to port {0} on {1} or {2}" 172 | else: 173 | msg = "Unable to connect to port {0} on {2}" 174 | super(NoValidConnectionsError, self).__init__( 175 | None, # stand-in for errno 176 | msg.format(addrs[0][1], body, tail) 177 | ) 178 | self.errors = errors 179 | 180 | def __reduce__(self): 181 | return (self.__class__, (self.errors, )) 182 | -------------------------------------------------------------------------------- /paramiko/rsakey.py: -------------------------------------------------------------------------------- 1 | # Copyright (C) 2003-2007 Robey Pointer 2 | # 3 | # This file is part of paramiko. 4 | # 5 | # Paramiko is free software; you can redistribute it and/or modify it under the 6 | # terms of the GNU Lesser General Public License as published by the Free 7 | # Software Foundation; either version 2.1 of the License, or (at your option) 8 | # any later version. 9 | # 10 | # Paramiko is distributed in the hope that it will be useful, but WITHOUT ANY 11 | # WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR 12 | # A PARTICULAR PURPOSE. See the GNU Lesser General Public License for more 13 | # details. 14 | # 15 | # You should have received a copy of the GNU Lesser General Public License 16 | # along with Paramiko; if not, write to the Free Software Foundation, Inc., 17 | # 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA. 18 | 19 | """ 20 | RSA keys. 21 | """ 22 | 23 | from cryptography.exceptions import InvalidSignature 24 | from cryptography.hazmat.backends import default_backend 25 | from cryptography.hazmat.primitives import hashes, serialization 26 | from cryptography.hazmat.primitives.asymmetric import rsa, padding 27 | 28 | from paramiko.message import Message 29 | from paramiko.pkey import PKey 30 | from paramiko.py3compat import PY2 31 | from paramiko.ssh_exception import SSHException 32 | 33 | 34 | class RSAKey(PKey): 35 | """ 36 | Representation of an RSA key which can be used to sign and verify SSH2 37 | data. 38 | """ 39 | 40 | def __init__(self, msg=None, data=None, filename=None, password=None, 41 | key=None, file_obj=None): 42 | self.key = None 43 | if file_obj is not None: 44 | self._from_private_key(file_obj, password) 45 | return 46 | if filename is not None: 47 | self._from_private_key_file(filename, password) 48 | return 49 | if (msg is None) and (data is not None): 50 | msg = Message(data) 51 | if key is not None: 52 | self.key = key 53 | else: 54 | if msg is None: 55 | raise SSHException('Key object may not be empty') 56 | if msg.get_text() != 'ssh-rsa': 57 | raise SSHException('Invalid key') 58 | self.key = rsa.RSAPublicNumbers( 59 | e=msg.get_mpint(), n=msg.get_mpint() 60 | ).public_key(default_backend()) 61 | 62 | @property 63 | def size(self): 64 | return self.key.key_size 65 | 66 | @property 67 | def public_numbers(self): 68 | if isinstance(self.key, rsa.RSAPrivateKey): 69 | return self.key.private_numbers().public_numbers 70 | else: 71 | return self.key.public_numbers() 72 | 73 | def asbytes(self): 74 | m = Message() 75 | m.add_string('ssh-rsa') 76 | m.add_mpint(self.public_numbers.e) 77 | m.add_mpint(self.public_numbers.n) 78 | return m.asbytes() 79 | 80 | def __str__(self): 81 | # NOTE: as per inane commentary in #853, this appears to be the least 82 | # crummy way to get a representation that prints identical to Python 83 | # 2's previous behavior, on both interpreters. 84 | # TODO: replace with a nice clean fingerprint display or something 85 | if PY2: 86 | # Can't just return the .decode below for Py2 because stuff still 87 | # tries stuffing it into ASCII for whatever godforsaken reason 88 | return self.asbytes() 89 | else: 90 | return self.asbytes().decode('utf8', errors='ignore') 91 | 92 | def __hash__(self): 93 | return hash((self.get_name(), self.public_numbers.e, 94 | self.public_numbers.n)) 95 | 96 | def get_name(self): 97 | return 'ssh-rsa' 98 | 99 | def get_bits(self): 100 | return self.size 101 | 102 | def can_sign(self): 103 | return isinstance(self.key, rsa.RSAPrivateKey) 104 | 105 | def sign_ssh_data(self, data): 106 | signer = self.key.signer( 107 | padding=padding.PKCS1v15(), 108 | algorithm=hashes.SHA1(), 109 | ) 110 | signer.update(data) 111 | sig = signer.finalize() 112 | 113 | m = Message() 114 | m.add_string('ssh-rsa') 115 | m.add_string(sig) 116 | return m 117 | 118 | def verify_ssh_sig(self, data, msg): 119 | if msg.get_text() != 'ssh-rsa': 120 | return False 121 | key = self.key 122 | if isinstance(key, rsa.RSAPrivateKey): 123 | key = key.public_key() 124 | 125 | verifier = key.verifier( 126 | signature=msg.get_binary(), 127 | padding=padding.PKCS1v15(), 128 | algorithm=hashes.SHA1(), 129 | ) 130 | verifier.update(data) 131 | try: 132 | verifier.verify() 133 | except InvalidSignature: 134 | return False 135 | else: 136 | return True 137 | 138 | def write_private_key_file(self, filename, password=None): 139 | self._write_private_key_file( 140 | filename, 141 | self.key, 142 | serialization.PrivateFormat.TraditionalOpenSSL, 143 | password=password 144 | ) 145 | 146 | def write_private_key(self, file_obj, password=None): 147 | self._write_private_key( 148 | file_obj, 149 | self.key, 150 | serialization.PrivateFormat.TraditionalOpenSSL, 151 | password=password 152 | ) 153 | 154 | @staticmethod 155 | def generate(bits, progress_func=None): 156 | """ 157 | Generate a new private RSA key. This factory function can be used to 158 | generate a new host key or authentication key. 159 | 160 | :param int bits: number of bits the generated key should be. 161 | :param progress_func: Unused 162 | :return: new `.RSAKey` private key 163 | """ 164 | key = rsa.generate_private_key( 165 | public_exponent=65537, key_size=bits, backend=default_backend() 166 | ) 167 | return RSAKey(key=key) 168 | 169 | # ...internals... 170 | 171 | def _from_private_key_file(self, filename, password): 172 | data = self._read_private_key_file('RSA', filename, password) 173 | self._decode_key(data) 174 | 175 | def _from_private_key(self, file_obj, password): 176 | data = self._read_private_key('RSA', file_obj, password) 177 | self._decode_key(data) 178 | 179 | def _decode_key(self, data): 180 | try: 181 | key = serialization.load_der_private_key( 182 | data, password=None, backend=default_backend() 183 | ) 184 | except ValueError as e: 185 | raise SSHException(str(e)) 186 | 187 | assert isinstance(key, rsa.RSAPrivateKey) 188 | self.key = key 189 | -------------------------------------------------------------------------------- /wetland/server/sftpServer.py: -------------------------------------------------------------------------------- 1 | import os 2 | import uuid 3 | from wetland.config import cfg 4 | 5 | from paramiko import ServerInterface, SFTPServerInterface, SFTPServer, \ 6 | SFTPAttributes, SFTPHandle, SFTP_OK, AUTH_SUCCESSFUL,\ 7 | OPEN_SUCCEEDED, SFTP_OP_UNSUPPORTED 8 | 9 | 10 | class remote_sftp_handle(SFTPHandle): 11 | def __init__(self, file_name, output, remote_file, save_file): 12 | self.remote_file = remote_file 13 | self.file_name = file_name 14 | self.save_file = save_file 15 | self.opt = output 16 | 17 | def close(self): 18 | self.opt.o("sftpfile", 'close', self.file_name) 19 | self.remote_file.close() 20 | if self.save_file: 21 | self.save_file.close() 22 | self.opt.o('upfile', 'sftp', self.file_name) 23 | self.opt.upfile(self.file_name) 24 | 25 | def read(self, offset, length): 26 | self.opt.o("sftpfile", 'read', self.file_name) 27 | if not self.remote_file.readable(): 28 | return SFTP_OP_UNSUPPORTED 29 | 30 | try: 31 | self.remote_file.seek(offset) 32 | data = self.remote_file.read(length) 33 | return data 34 | except IOError as e: 35 | return SFTPServer.convert_errno(e.errno) 36 | 37 | def write(self, offset, data): 38 | self.opt.o("sftpfile", 'write', self.file_name) 39 | if not self.remote_file.writable(): 40 | return SFTP_OP_UNSUPPORTED 41 | 42 | try: 43 | self.remote_file.seek(offset) 44 | self.remote_file.write(data) 45 | self.remote_file.flush() 46 | 47 | self.save_file.seek(offset) 48 | self.save_file.write(data) 49 | self.save_file.flush() 50 | except IOError as e: 51 | return SFTPServer.convert_errno(e.errno) 52 | return SFTP_OK 53 | 54 | def stat(self): 55 | self.opt.o("sftpfile", 'stat', self.file_name) 56 | try: 57 | return self.remote_file.stat() 58 | except IOError as e: 59 | return SFTPServer.convert_errno(e.errno) 60 | else: 61 | return SFTP_OK 62 | 63 | def chattr(self, attr): 64 | self.opt.o("sftpfile", 'chattr', self.file_name) 65 | try: 66 | self.remote_file.chattr(attr) 67 | 68 | except IOError as e: 69 | return SFTPServer.convert_errno(e.errno) 70 | else: 71 | return SFTP_OK 72 | 73 | 74 | class sftp_server (SFTPServerInterface): 75 | def __init__(self, ssh_server): 76 | self.ssh_server = ssh_server 77 | self.docker_client = ssh_server.docker_trans.open_sftp_client() 78 | self.root = self.docker_client.getcwd() 79 | 80 | self.cfg = cfg 81 | self.opt = ssh_server.opt 82 | self.opt.o("sftpserver", 'init', 'success') 83 | 84 | def canonicalize(self, path): 85 | return path 86 | 87 | def list_folder(self, path): 88 | self.opt.o("sftpserver", 'list', path) 89 | try: 90 | return self.docker_client.listdir_attr(path) 91 | except IOError as e: 92 | return SFTPServer.convert_errno(e.errno) 93 | 94 | def stat(self, path): 95 | self.opt.o("sftpserver", 'stat', path) 96 | try: 97 | return self.docker_client.stat(path) 98 | except IOError as e: 99 | return SFTPServer.convert_errno(e.errno) 100 | 101 | def lstat(self, path): 102 | self.opt.o("sftpserver", 'lstat', path) 103 | try: 104 | return self.docker_client.lstat(path) 105 | except IOError as e: 106 | return SFTPServer.convert_errno(e.errno) 107 | 108 | def open(self, path, flags, attr): 109 | 110 | binary_flag = getattr(os, 'O_BINARY', 0) 111 | flags |= binary_flag 112 | save_file = None 113 | 114 | if (flags & os.O_CREAT) and (attr is not None): 115 | attr._flags &= ~attr.FLAG_PERMISSIONS 116 | self.chattr(path, attr) 117 | 118 | if flags & os.O_WRONLY: 119 | if flags & os.O_APPEND: 120 | fstr = 'ab' 121 | else: 122 | fstr = 'wb' 123 | save_dir = self.cfg.get("files", "path") 124 | # file_name = [self.ssh_server.hacker_ip] 125 | # file_name.append(time.strftime('%Y%m%d%H%M%S')) 126 | # file_name.append(re.sub('[^A-Za-z0-0]', '-', path)) 127 | # file_name = '_'.join(file_name) 128 | file_name = str(uuid.uuid1()).replace('-', '') 129 | save_path = os.path.join(save_dir, file_name) 130 | if not os.path.exists(save_dir): 131 | os.makedirs(save_dir) 132 | save_file = open(save_path, fstr) 133 | 134 | elif flags & os.O_RDWR: 135 | if flags & os.O_APPEND: 136 | fstr = 'a+b' 137 | else: 138 | fstr = 'r+b' 139 | else: 140 | # O_RDONLY (== 0) 141 | fstr = 'rb' 142 | 143 | self.opt.o("sftpserver", 'open', path) 144 | try: 145 | remote_file = self.docker_client.file(path, fstr) 146 | except IOError as e: 147 | return SFTPServer.convert_errno(e.errno) 148 | 149 | fobj = remote_sftp_handle(path, self.opt, remote_file, save_file) 150 | return fobj 151 | 152 | def remove(self, path): 153 | self.opt.o("sftpserver", 'remove', path) 154 | try: 155 | self.docker_client.remove(path) 156 | except IOError as e: 157 | return SFTPServer.convert_errno(e.errno) 158 | else: 159 | return SFTP_OK 160 | 161 | def rename(self, oldpath, newpath): 162 | self.opt.o("sftpserver", 'rename', '->'.join((oldpath, newpath))) 163 | try: 164 | self.docker_client.rename(oldpath, newpath) 165 | except IOError as e: 166 | return SFTPServer.convert_errno(e.errno) 167 | else: 168 | return SFTP_OK 169 | 170 | def mkdir(self, path, attr): 171 | self.opt.o("sftpserver", 'mkdir', path) 172 | try: 173 | self.docker_client.mkdir(path) 174 | except IOError as e: 175 | return SFTPServer.convert_errno(e.errno) 176 | else: 177 | return SFTP_OK 178 | 179 | def rmdir(self, path): 180 | self.opt.o("sftpserver", 'rmdir', path) 181 | try: 182 | self.docker_client.rmdir(path) 183 | except IOError as e: 184 | return SFTPServer.convert_errno(e.errno) 185 | return SFTP_OK 186 | 187 | def chattr(self, path, attr): 188 | self.opt.o("sftpserver", 'chattr', path) 189 | try: 190 | if attr._flags & attr.FLAG_PERMISSIONS: 191 | self.docker_client.chmod(path, attr.st_mode) 192 | 193 | if attr._flags & attr.FLAG_UIDGID: 194 | self.docker_client.chown(path, attr.st_uid, attr.st_gid) 195 | 196 | if attr._flags & attr.FLAG_AMTIME: 197 | self.docker_client.utime(path, 198 | (attr.st_atime, attr.st_mtime)) 199 | 200 | if attr._flags & attr.FLAG_SIZE: 201 | self.docker_client.truncate(path, attr.st_size) 202 | 203 | except IOError as e: 204 | return SFTPServer.convert_errno(e.errno) 205 | else: 206 | return SFTP_OK 207 | 208 | def symlink(self, target_path, path): 209 | self.opt.o("sftpserver", 'symlink', '->'.join(target_path, path)) 210 | try: 211 | self.docker_client.symlink(target_path, path) 212 | except IOError as e: 213 | return SFTPServer.convert_errno(e.errno) 214 | return SFTP_OK 215 | 216 | def readlink(self, path): 217 | self.opt.o("sftpserver", 'readlink', path) 218 | try: 219 | symlink = self.docker_client.readlink(path) 220 | except IOError as e: 221 | return SFTPServer.convert_errno(e.errno) 222 | return symlink 223 | -------------------------------------------------------------------------------- /paramiko/ed25519key.py: -------------------------------------------------------------------------------- 1 | # This file is part of paramiko. 2 | # 3 | # Paramiko is free software; you can redistribute it and/or modify it under the 4 | # terms of the GNU Lesser General Public License as published by the Free 5 | # Software Foundation; either version 2.1 of the License, or (at your option) 6 | # any later version. 7 | # 8 | # Paramiko is distrubuted in the hope that it will be useful, but WITHOUT ANY 9 | # WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR 10 | # A PARTICULAR PURPOSE. See the GNU Lesser General Public License for more 11 | # details. 12 | # 13 | # You should have received a copy of the GNU Lesser General Public License 14 | # along with Paramiko; if not, write to the Free Software Foundation, Inc., 15 | # 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA. 16 | 17 | import bcrypt 18 | 19 | from cryptography.hazmat.backends import default_backend 20 | from cryptography.hazmat.primitives.ciphers import Cipher 21 | 22 | import nacl.signing 23 | 24 | import six 25 | 26 | from paramiko.message import Message 27 | from paramiko.pkey import PKey 28 | from paramiko.ssh_exception import SSHException, PasswordRequiredException 29 | 30 | 31 | OPENSSH_AUTH_MAGIC = b"openssh-key-v1\x00" 32 | 33 | 34 | def unpad(data): 35 | # At the moment, this is only used for unpadding private keys on disk. This 36 | # really ought to be made constant time (possibly by upstreaming this logic 37 | # into pyca/cryptography). 38 | padding_length = six.indexbytes(data, -1) 39 | if padding_length > 16: 40 | raise SSHException("Invalid key") 41 | for i in range(1, padding_length + 1): 42 | if six.indexbytes(data, -i) != (padding_length - i + 1): 43 | raise SSHException("Invalid key") 44 | return data[:-padding_length] 45 | 46 | 47 | class Ed25519Key(PKey): 48 | def __init__(self, msg=None, data=None, filename=None, password=None): 49 | verifying_key = signing_key = None 50 | if msg is None and data is not None: 51 | msg = Message(data) 52 | if msg is not None: 53 | if msg.get_text() != "ssh-ed25519": 54 | raise SSHException("Invalid key") 55 | verifying_key = nacl.signing.VerifyKey(msg.get_binary()) 56 | elif filename is not None: 57 | with open(filename, "r") as f: 58 | data = self._read_private_key("OPENSSH", f) 59 | signing_key = self._parse_signing_key_data(data, password) 60 | 61 | if signing_key is None and verifying_key is None: 62 | raise ValueError("need a key") 63 | 64 | self._signing_key = signing_key 65 | self._verifying_key = verifying_key 66 | 67 | def _parse_signing_key_data(self, data, password): 68 | from paramiko.transport import Transport 69 | # We may eventually want this to be usable for other key types, as 70 | # OpenSSH moves to it, but for now this is just for Ed25519 keys. 71 | # This format is described here: 72 | # https://github.com/openssh/openssh-portable/blob/master/PROTOCOL.key 73 | # The description isn't totally complete, and I had to refer to the 74 | # source for a full implementation. 75 | message = Message(data) 76 | if message.get_bytes(len(OPENSSH_AUTH_MAGIC)) != OPENSSH_AUTH_MAGIC: 77 | raise SSHException("Invalid key") 78 | 79 | ciphername = message.get_text() 80 | kdfname = message.get_text() 81 | kdfoptions = message.get_binary() 82 | num_keys = message.get_int() 83 | 84 | if kdfname == "none": 85 | # kdfname of "none" must have an empty kdfoptions, the ciphername 86 | # must be "none" 87 | if kdfoptions or ciphername != "none": 88 | raise SSHException("Invalid key") 89 | elif kdfname == "bcrypt": 90 | if not password: 91 | raise PasswordRequiredException( 92 | "Private key file is encrypted" 93 | ) 94 | kdf = Message(kdfoptions) 95 | bcrypt_salt = kdf.get_binary() 96 | bcrypt_rounds = kdf.get_int() 97 | else: 98 | raise SSHException("Invalid key") 99 | 100 | if ciphername != "none" and ciphername not in Transport._cipher_info: 101 | raise SSHException("Invalid key") 102 | 103 | public_keys = [] 104 | for _ in range(num_keys): 105 | pubkey = Message(message.get_binary()) 106 | if pubkey.get_text() != "ssh-ed25519": 107 | raise SSHException("Invalid key") 108 | public_keys.append(pubkey.get_binary()) 109 | 110 | private_ciphertext = message.get_binary() 111 | if ciphername == "none": 112 | private_data = private_ciphertext 113 | else: 114 | cipher = Transport._cipher_info[ciphername] 115 | key = bcrypt.kdf( 116 | password=password, 117 | salt=bcrypt_salt, 118 | desired_key_bytes=cipher["key-size"] + cipher["block-size"], 119 | rounds=bcrypt_rounds, 120 | # We can't control how many rounds are on disk, so no sense 121 | # warning about it. 122 | ignore_few_rounds=True, 123 | ) 124 | decryptor = Cipher( 125 | cipher["class"](key[:cipher["key-size"]]), 126 | cipher["mode"](key[cipher["key-size"]:]), 127 | backend=default_backend() 128 | ).decryptor() 129 | private_data = ( 130 | decryptor.update(private_ciphertext) + decryptor.finalize() 131 | ) 132 | 133 | message = Message(unpad(private_data)) 134 | if message.get_int() != message.get_int(): 135 | raise SSHException("Invalid key") 136 | 137 | signing_keys = [] 138 | for i in range(num_keys): 139 | if message.get_text() != "ssh-ed25519": 140 | raise SSHException("Invalid key") 141 | # A copy of the public key, again, ignore. 142 | public = message.get_binary() 143 | key_data = message.get_binary() 144 | # The second half of the key data is yet another copy of the public 145 | # key... 146 | signing_key = nacl.signing.SigningKey(key_data[:32]) 147 | # Verify that all the public keys are the same... 148 | assert ( 149 | signing_key.verify_key.encode() == public == public_keys[i] == 150 | key_data[32:] 151 | ) 152 | signing_keys.append(signing_key) 153 | # Comment, ignore. 154 | message.get_binary() 155 | 156 | if len(signing_keys) != 1: 157 | raise SSHException("Invalid key") 158 | return signing_keys[0] 159 | 160 | def asbytes(self): 161 | if self.can_sign(): 162 | v = self._signing_key.verify_key 163 | else: 164 | v = self._verifying_key 165 | m = Message() 166 | m.add_string("ssh-ed25519") 167 | m.add_string(v.encode()) 168 | return m.asbytes() 169 | 170 | def __hash__(self): 171 | if self.can_sign(): 172 | v = self._signing_key.verify_key 173 | else: 174 | v = self._verifying_key 175 | return hash((self.get_name(), v)) 176 | 177 | def get_name(self): 178 | return "ssh-ed25519" 179 | 180 | def get_bits(self): 181 | return 256 182 | 183 | def can_sign(self): 184 | return self._signing_key is not None 185 | 186 | def sign_ssh_data(self, data): 187 | m = Message() 188 | m.add_string("ssh-ed25519") 189 | m.add_string(self._signing_key.sign(data).signature) 190 | return m 191 | 192 | def verify_ssh_sig(self, data, msg): 193 | if msg.get_text() != "ssh-ed25519": 194 | return False 195 | 196 | try: 197 | self._verifying_key.verify(data, msg.get_binary()) 198 | except nacl.exceptions.BadSignatureError: 199 | return False 200 | else: 201 | return True 202 | -------------------------------------------------------------------------------- /paramiko/sftp_handle.py: -------------------------------------------------------------------------------- 1 | # Copyright (C) 2003-2007 Robey Pointer 2 | # 3 | # This file is part of paramiko. 4 | # 5 | # Paramiko is free software; you can redistribute it and/or modify it under the 6 | # terms of the GNU Lesser General Public License as published by the Free 7 | # Software Foundation; either version 2.1 of the License, or (at your option) 8 | # any later version. 9 | # 10 | # Paramiko is distributed in the hope that it will be useful, but WITHOUT ANY 11 | # WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR 12 | # A PARTICULAR PURPOSE. See the GNU Lesser General Public License for more 13 | # details. 14 | # 15 | # You should have received a copy of the GNU Lesser General Public License 16 | # along with Paramiko; if not, write to the Free Software Foundation, Inc., 17 | # 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA. 18 | 19 | """ 20 | Abstraction of an SFTP file handle (for server mode). 21 | """ 22 | 23 | import os 24 | from paramiko.sftp import SFTP_OP_UNSUPPORTED, SFTP_OK 25 | from paramiko.util import ClosingContextManager 26 | 27 | 28 | class SFTPHandle (ClosingContextManager): 29 | """ 30 | Abstract object representing a handle to an open file (or folder) in an 31 | SFTP server implementation. Each handle has a string representation used 32 | by the client to refer to the underlying file. 33 | 34 | Server implementations can (and should) subclass SFTPHandle to implement 35 | features of a file handle, like `stat` or `chattr`. 36 | 37 | Instances of this class may be used as context managers. 38 | """ 39 | def __init__(self, flags=0): 40 | """ 41 | Create a new file handle representing a local file being served over 42 | SFTP. If ``flags`` is passed in, it's used to determine if the file 43 | is open in append mode. 44 | 45 | :param int flags: optional flags as passed to 46 | `.SFTPServerInterface.open` 47 | """ 48 | self.__flags = flags 49 | self.__name = None 50 | # only for handles to folders: 51 | self.__files = {} 52 | self.__tell = None 53 | 54 | def close(self): 55 | """ 56 | When a client closes a file, this method is called on the handle. 57 | Normally you would use this method to close the underlying OS level 58 | file object(s). 59 | 60 | The default implementation checks for attributes on ``self`` named 61 | ``readfile`` and/or ``writefile``, and if either or both are present, 62 | their ``close()`` methods are called. This means that if you are 63 | using the default implementations of `read` and `write`, this 64 | method's default implementation should be fine also. 65 | """ 66 | readfile = getattr(self, 'readfile', None) 67 | if readfile is not None: 68 | readfile.close() 69 | writefile = getattr(self, 'writefile', None) 70 | if writefile is not None: 71 | writefile.close() 72 | 73 | def read(self, offset, length): 74 | """ 75 | Read up to ``length`` bytes from this file, starting at position 76 | ``offset``. The offset may be a Python long, since SFTP allows it 77 | to be 64 bits. 78 | 79 | If the end of the file has been reached, this method may return an 80 | empty string to signify EOF, or it may also return ``SFTP_EOF``. 81 | 82 | The default implementation checks for an attribute on ``self`` named 83 | ``readfile``, and if present, performs the read operation on the Python 84 | file-like object found there. (This is meant as a time saver for the 85 | common case where you are wrapping a Python file object.) 86 | 87 | :param offset: position in the file to start reading from. 88 | :param int length: number of bytes to attempt to read. 89 | :return: data read from the file, or an SFTP error code, as a `str`. 90 | """ 91 | readfile = getattr(self, 'readfile', None) 92 | if readfile is None: 93 | return SFTP_OP_UNSUPPORTED 94 | try: 95 | if self.__tell is None: 96 | self.__tell = readfile.tell() 97 | if offset != self.__tell: 98 | readfile.seek(offset) 99 | self.__tell = offset 100 | data = readfile.read(length) 101 | except IOError as e: 102 | self.__tell = None 103 | return SFTPServer.convert_errno(e.errno) 104 | self.__tell += len(data) 105 | return data 106 | 107 | def write(self, offset, data): 108 | """ 109 | Write ``data`` into this file at position ``offset``. Extending the 110 | file past its original end is expected. Unlike Python's normal 111 | ``write()`` methods, this method cannot do a partial write: it must 112 | write all of ``data`` or else return an error. 113 | 114 | The default implementation checks for an attribute on ``self`` named 115 | ``writefile``, and if present, performs the write operation on the 116 | Python file-like object found there. The attribute is named 117 | differently from ``readfile`` to make it easy to implement read-only 118 | (or write-only) files, but if both attributes are present, they should 119 | refer to the same file. 120 | 121 | :param offset: position in the file to start reading from. 122 | :param str data: data to write into the file. 123 | :return: an SFTP error code like ``SFTP_OK``. 124 | """ 125 | writefile = getattr(self, 'writefile', None) 126 | if writefile is None: 127 | return SFTP_OP_UNSUPPORTED 128 | try: 129 | # in append mode, don't care about seeking 130 | if (self.__flags & os.O_APPEND) == 0: 131 | if self.__tell is None: 132 | self.__tell = writefile.tell() 133 | if offset != self.__tell: 134 | writefile.seek(offset) 135 | self.__tell = offset 136 | writefile.write(data) 137 | writefile.flush() 138 | except IOError as e: 139 | self.__tell = None 140 | return SFTPServer.convert_errno(e.errno) 141 | if self.__tell is not None: 142 | self.__tell += len(data) 143 | return SFTP_OK 144 | 145 | def stat(self): 146 | """ 147 | Return an `.SFTPAttributes` object referring to this open file, or an 148 | error code. This is equivalent to `.SFTPServerInterface.stat`, except 149 | it's called on an open file instead of a path. 150 | 151 | :return: 152 | an attributes object for the given file, or an SFTP error code 153 | (like ``SFTP_PERMISSION_DENIED``). 154 | :rtype: `.SFTPAttributes` or error code 155 | """ 156 | return SFTP_OP_UNSUPPORTED 157 | 158 | def chattr(self, attr): 159 | """ 160 | Change the attributes of this file. The ``attr`` object will contain 161 | only those fields provided by the client in its request, so you should 162 | check for the presence of fields before using them. 163 | 164 | :param .SFTPAttributes attr: the attributes to change on this file. 165 | :return: an `int` error code like ``SFTP_OK``. 166 | """ 167 | return SFTP_OP_UNSUPPORTED 168 | 169 | # ...internals... 170 | 171 | def _set_files(self, files): 172 | """ 173 | Used by the SFTP server code to cache a directory listing. (In 174 | the SFTP protocol, listing a directory is a multi-stage process 175 | requiring a temporary handle.) 176 | """ 177 | self.__files = files 178 | 179 | def _get_next_files(self): 180 | """ 181 | Used by the SFTP server code to retrieve a cached directory 182 | listing. 183 | """ 184 | fnlist = self.__files[:16] 185 | self.__files = self.__files[16:] 186 | return fnlist 187 | 188 | def _get_name(self): 189 | return self.__name 190 | 191 | def _set_name(self, name): 192 | self.__name = name 193 | 194 | 195 | from paramiko.sftp_server import SFTPServer 196 | -------------------------------------------------------------------------------- /paramiko/buffered_pipe.py: -------------------------------------------------------------------------------- 1 | # Copyright (C) 2006-2007 Robey Pointer 2 | # 3 | # This file is part of paramiko. 4 | # 5 | # Paramiko is free software; you can redistribute it and/or modify it under the 6 | # terms of the GNU Lesser General Public License as published by the Free 7 | # Software Foundation; either version 2.1 of the License, or (at your option) 8 | # any later version. 9 | # 10 | # Paramiko is distributed in the hope that it will be useful, but WITHOUT ANY 11 | # WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR 12 | # A PARTICULAR PURPOSE. See the GNU Lesser General Public License for more 13 | # details. 14 | # 15 | # You should have received a copy of the GNU Lesser General Public License 16 | # along with Paramiko; if not, write to the Free Software Foundation, Inc., 17 | # 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA. 18 | 19 | """ 20 | Attempt to generalize the "feeder" part of a `.Channel`: an object which can be 21 | read from and closed, but is reading from a buffer fed by another thread. The 22 | read operations are blocking and can have a timeout set. 23 | """ 24 | 25 | import array 26 | import threading 27 | import time 28 | from paramiko.py3compat import PY2, b 29 | 30 | 31 | class PipeTimeout (IOError): 32 | """ 33 | Indicates that a timeout was reached on a read from a `.BufferedPipe`. 34 | """ 35 | pass 36 | 37 | 38 | class BufferedPipe (object): 39 | """ 40 | A buffer that obeys normal read (with timeout) & close semantics for a 41 | file or socket, but is fed data from another thread. This is used by 42 | `.Channel`. 43 | """ 44 | 45 | def __init__(self): 46 | self._lock = threading.Lock() 47 | self._cv = threading.Condition(self._lock) 48 | self._event = None 49 | self._buffer = array.array('B') 50 | self._closed = False 51 | 52 | if PY2: 53 | def _buffer_frombytes(self, data): 54 | self._buffer.fromstring(data) 55 | 56 | def _buffer_tobytes(self, limit=None): 57 | return self._buffer[:limit].tostring() 58 | else: 59 | def _buffer_frombytes(self, data): 60 | self._buffer.frombytes(data) 61 | 62 | def _buffer_tobytes(self, limit=None): 63 | return self._buffer[:limit].tobytes() 64 | 65 | def set_event(self, event): 66 | """ 67 | Set an event on this buffer. When data is ready to be read (or the 68 | buffer has been closed), the event will be set. When no data is 69 | ready, the event will be cleared. 70 | 71 | :param threading.Event event: the event to set/clear 72 | """ 73 | self._lock.acquire() 74 | try: 75 | self._event = event 76 | # Make sure the event starts in `set` state if we appear to already 77 | # be closed; otherwise, if we start in `clear` state & are closed, 78 | # nothing will ever call `.feed` and the event (& OS pipe, if we're 79 | # wrapping one - see `Channel.fileno`) will permanently stay in 80 | # `clear`, causing deadlock if e.g. `select`ed upon. 81 | if self._closed or len(self._buffer) > 0: 82 | event.set() 83 | else: 84 | event.clear() 85 | finally: 86 | self._lock.release() 87 | 88 | def feed(self, data): 89 | """ 90 | Feed new data into this pipe. This method is assumed to be called 91 | from a separate thread, so synchronization is done. 92 | 93 | :param data: the data to add, as a ``str`` or ``bytes`` 94 | """ 95 | self._lock.acquire() 96 | try: 97 | if self._event is not None: 98 | self._event.set() 99 | self._buffer_frombytes(b(data)) 100 | self._cv.notifyAll() 101 | finally: 102 | self._lock.release() 103 | 104 | def read_ready(self): 105 | """ 106 | Returns true if data is buffered and ready to be read from this 107 | feeder. A ``False`` result does not mean that the feeder has closed; 108 | it means you may need to wait before more data arrives. 109 | 110 | :return: 111 | ``True`` if a `read` call would immediately return at least one 112 | byte; ``False`` otherwise. 113 | """ 114 | self._lock.acquire() 115 | try: 116 | if len(self._buffer) == 0: 117 | return False 118 | return True 119 | finally: 120 | self._lock.release() 121 | 122 | def read(self, nbytes, timeout=None): 123 | """ 124 | Read data from the pipe. The return value is a string representing 125 | the data received. The maximum amount of data to be received at once 126 | is specified by ``nbytes``. If a string of length zero is returned, 127 | the pipe has been closed. 128 | 129 | The optional ``timeout`` argument can be a nonnegative float expressing 130 | seconds, or ``None`` for no timeout. If a float is given, a 131 | `.PipeTimeout` will be raised if the timeout period value has elapsed 132 | before any data arrives. 133 | 134 | :param int nbytes: maximum number of bytes to read 135 | :param float timeout: 136 | maximum seconds to wait (or ``None``, the default, to wait forever) 137 | :return: the read data, as a ``str`` or ``bytes`` 138 | 139 | :raises: 140 | `.PipeTimeout` -- if a timeout was specified and no data was ready 141 | before that timeout 142 | """ 143 | out = bytes() 144 | self._lock.acquire() 145 | try: 146 | if len(self._buffer) == 0: 147 | if self._closed: 148 | return out 149 | # should we block? 150 | if timeout == 0.0: 151 | raise PipeTimeout() 152 | # loop here in case we get woken up but a different thread has 153 | # grabbed everything in the buffer. 154 | while (len(self._buffer) == 0) and not self._closed: 155 | then = time.time() 156 | self._cv.wait(timeout) 157 | if timeout is not None: 158 | timeout -= time.time() - then 159 | if timeout <= 0.0: 160 | raise PipeTimeout() 161 | 162 | # something's in the buffer and we have the lock! 163 | if len(self._buffer) <= nbytes: 164 | out = self._buffer_tobytes() 165 | del self._buffer[:] 166 | if (self._event is not None) and not self._closed: 167 | self._event.clear() 168 | else: 169 | out = self._buffer_tobytes(nbytes) 170 | del self._buffer[:nbytes] 171 | finally: 172 | self._lock.release() 173 | 174 | return out 175 | 176 | def empty(self): 177 | """ 178 | Clear out the buffer and return all data that was in it. 179 | 180 | :return: 181 | any data that was in the buffer prior to clearing it out, as a 182 | `str` 183 | """ 184 | self._lock.acquire() 185 | try: 186 | out = self._buffer_tobytes() 187 | del self._buffer[:] 188 | if (self._event is not None) and not self._closed: 189 | self._event.clear() 190 | return out 191 | finally: 192 | self._lock.release() 193 | 194 | def close(self): 195 | """ 196 | Close this pipe object. Future calls to `read` after the buffer 197 | has been emptied will return immediately with an empty string. 198 | """ 199 | self._lock.acquire() 200 | try: 201 | self._closed = True 202 | self._cv.notifyAll() 203 | if self._event is not None: 204 | self._event.set() 205 | finally: 206 | self._lock.release() 207 | 208 | def __len__(self): 209 | """ 210 | Return the number of bytes buffered. 211 | 212 | :return: number (`int`) of bytes buffered 213 | """ 214 | self._lock.acquire() 215 | try: 216 | return len(self._buffer) 217 | finally: 218 | self._lock.release() 219 | -------------------------------------------------------------------------------- /paramiko/common.py: -------------------------------------------------------------------------------- 1 | # Copyright (C) 2003-2007 Robey Pointer 2 | # 3 | # This file is part of paramiko. 4 | # 5 | # Paramiko is free software; you can redistribute it and/or modify it under the 6 | # terms of the GNU Lesser General Public License as published by the Free 7 | # Software Foundation; either version 2.1 of the License, or (at your option) 8 | # any later version. 9 | # 10 | # Paramiko is distributed in the hope that it will be useful, but WITHOUT ANY 11 | # WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR 12 | # A PARTICULAR PURPOSE. See the GNU Lesser General Public License for more 13 | # details. 14 | # 15 | # You should have received a copy of the GNU Lesser General Public License 16 | # along with Paramiko; if not, write to the Free Software Foundation, Inc., 17 | # 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA. 18 | 19 | """ 20 | Common constants and global variables. 21 | """ 22 | import logging 23 | from paramiko.py3compat import byte_chr, PY2, bytes_types, text_type, long 24 | 25 | MSG_DISCONNECT, MSG_IGNORE, MSG_UNIMPLEMENTED, MSG_DEBUG, \ 26 | MSG_SERVICE_REQUEST, MSG_SERVICE_ACCEPT = range(1, 7) 27 | MSG_KEXINIT, MSG_NEWKEYS = range(20, 22) 28 | MSG_USERAUTH_REQUEST, MSG_USERAUTH_FAILURE, MSG_USERAUTH_SUCCESS, \ 29 | MSG_USERAUTH_BANNER = range(50, 54) 30 | MSG_USERAUTH_PK_OK = 60 31 | MSG_USERAUTH_INFO_REQUEST, MSG_USERAUTH_INFO_RESPONSE = range(60, 62) 32 | MSG_USERAUTH_GSSAPI_RESPONSE, MSG_USERAUTH_GSSAPI_TOKEN = range(60, 62) 33 | MSG_USERAUTH_GSSAPI_EXCHANGE_COMPLETE, MSG_USERAUTH_GSSAPI_ERROR,\ 34 | MSG_USERAUTH_GSSAPI_ERRTOK, MSG_USERAUTH_GSSAPI_MIC = range(63, 67) 35 | MSG_GLOBAL_REQUEST, MSG_REQUEST_SUCCESS, MSG_REQUEST_FAILURE = range(80, 83) 36 | MSG_CHANNEL_OPEN, MSG_CHANNEL_OPEN_SUCCESS, MSG_CHANNEL_OPEN_FAILURE, \ 37 | MSG_CHANNEL_WINDOW_ADJUST, MSG_CHANNEL_DATA, MSG_CHANNEL_EXTENDED_DATA, \ 38 | MSG_CHANNEL_EOF, MSG_CHANNEL_CLOSE, MSG_CHANNEL_REQUEST, \ 39 | MSG_CHANNEL_SUCCESS, MSG_CHANNEL_FAILURE = range(90, 101) 40 | 41 | cMSG_DISCONNECT = byte_chr(MSG_DISCONNECT) 42 | cMSG_IGNORE = byte_chr(MSG_IGNORE) 43 | cMSG_UNIMPLEMENTED = byte_chr(MSG_UNIMPLEMENTED) 44 | cMSG_DEBUG = byte_chr(MSG_DEBUG) 45 | cMSG_SERVICE_REQUEST = byte_chr(MSG_SERVICE_REQUEST) 46 | cMSG_SERVICE_ACCEPT = byte_chr(MSG_SERVICE_ACCEPT) 47 | cMSG_KEXINIT = byte_chr(MSG_KEXINIT) 48 | cMSG_NEWKEYS = byte_chr(MSG_NEWKEYS) 49 | cMSG_USERAUTH_REQUEST = byte_chr(MSG_USERAUTH_REQUEST) 50 | cMSG_USERAUTH_FAILURE = byte_chr(MSG_USERAUTH_FAILURE) 51 | cMSG_USERAUTH_SUCCESS = byte_chr(MSG_USERAUTH_SUCCESS) 52 | cMSG_USERAUTH_BANNER = byte_chr(MSG_USERAUTH_BANNER) 53 | cMSG_USERAUTH_PK_OK = byte_chr(MSG_USERAUTH_PK_OK) 54 | cMSG_USERAUTH_INFO_REQUEST = byte_chr(MSG_USERAUTH_INFO_REQUEST) 55 | cMSG_USERAUTH_INFO_RESPONSE = byte_chr(MSG_USERAUTH_INFO_RESPONSE) 56 | cMSG_USERAUTH_GSSAPI_RESPONSE = byte_chr(MSG_USERAUTH_GSSAPI_RESPONSE) 57 | cMSG_USERAUTH_GSSAPI_TOKEN = byte_chr(MSG_USERAUTH_GSSAPI_TOKEN) 58 | cMSG_USERAUTH_GSSAPI_EXCHANGE_COMPLETE = \ 59 | byte_chr(MSG_USERAUTH_GSSAPI_EXCHANGE_COMPLETE) 60 | cMSG_USERAUTH_GSSAPI_ERROR = byte_chr(MSG_USERAUTH_GSSAPI_ERROR) 61 | cMSG_USERAUTH_GSSAPI_ERRTOK = byte_chr(MSG_USERAUTH_GSSAPI_ERRTOK) 62 | cMSG_USERAUTH_GSSAPI_MIC = byte_chr(MSG_USERAUTH_GSSAPI_MIC) 63 | cMSG_GLOBAL_REQUEST = byte_chr(MSG_GLOBAL_REQUEST) 64 | cMSG_REQUEST_SUCCESS = byte_chr(MSG_REQUEST_SUCCESS) 65 | cMSG_REQUEST_FAILURE = byte_chr(MSG_REQUEST_FAILURE) 66 | cMSG_CHANNEL_OPEN = byte_chr(MSG_CHANNEL_OPEN) 67 | cMSG_CHANNEL_OPEN_SUCCESS = byte_chr(MSG_CHANNEL_OPEN_SUCCESS) 68 | cMSG_CHANNEL_OPEN_FAILURE = byte_chr(MSG_CHANNEL_OPEN_FAILURE) 69 | cMSG_CHANNEL_WINDOW_ADJUST = byte_chr(MSG_CHANNEL_WINDOW_ADJUST) 70 | cMSG_CHANNEL_DATA = byte_chr(MSG_CHANNEL_DATA) 71 | cMSG_CHANNEL_EXTENDED_DATA = byte_chr(MSG_CHANNEL_EXTENDED_DATA) 72 | cMSG_CHANNEL_EOF = byte_chr(MSG_CHANNEL_EOF) 73 | cMSG_CHANNEL_CLOSE = byte_chr(MSG_CHANNEL_CLOSE) 74 | cMSG_CHANNEL_REQUEST = byte_chr(MSG_CHANNEL_REQUEST) 75 | cMSG_CHANNEL_SUCCESS = byte_chr(MSG_CHANNEL_SUCCESS) 76 | cMSG_CHANNEL_FAILURE = byte_chr(MSG_CHANNEL_FAILURE) 77 | 78 | # for debugging: 79 | MSG_NAMES = { 80 | MSG_DISCONNECT: 'disconnect', 81 | MSG_IGNORE: 'ignore', 82 | MSG_UNIMPLEMENTED: 'unimplemented', 83 | MSG_DEBUG: 'debug', 84 | MSG_SERVICE_REQUEST: 'service-request', 85 | MSG_SERVICE_ACCEPT: 'service-accept', 86 | MSG_KEXINIT: 'kexinit', 87 | MSG_NEWKEYS: 'newkeys', 88 | 30: 'kex30', 89 | 31: 'kex31', 90 | 32: 'kex32', 91 | 33: 'kex33', 92 | 34: 'kex34', 93 | 40: 'kex40', 94 | 41: 'kex41', 95 | MSG_USERAUTH_REQUEST: 'userauth-request', 96 | MSG_USERAUTH_FAILURE: 'userauth-failure', 97 | MSG_USERAUTH_SUCCESS: 'userauth-success', 98 | MSG_USERAUTH_BANNER: 'userauth--banner', 99 | MSG_USERAUTH_PK_OK: 'userauth-60(pk-ok/info-request)', 100 | MSG_USERAUTH_INFO_RESPONSE: 'userauth-info-response', 101 | MSG_GLOBAL_REQUEST: 'global-request', 102 | MSG_REQUEST_SUCCESS: 'request-success', 103 | MSG_REQUEST_FAILURE: 'request-failure', 104 | MSG_CHANNEL_OPEN: 'channel-open', 105 | MSG_CHANNEL_OPEN_SUCCESS: 'channel-open-success', 106 | MSG_CHANNEL_OPEN_FAILURE: 'channel-open-failure', 107 | MSG_CHANNEL_WINDOW_ADJUST: 'channel-window-adjust', 108 | MSG_CHANNEL_DATA: 'channel-data', 109 | MSG_CHANNEL_EXTENDED_DATA: 'channel-extended-data', 110 | MSG_CHANNEL_EOF: 'channel-eof', 111 | MSG_CHANNEL_CLOSE: 'channel-close', 112 | MSG_CHANNEL_REQUEST: 'channel-request', 113 | MSG_CHANNEL_SUCCESS: 'channel-success', 114 | MSG_CHANNEL_FAILURE: 'channel-failure', 115 | MSG_USERAUTH_GSSAPI_RESPONSE: 'userauth-gssapi-response', 116 | MSG_USERAUTH_GSSAPI_TOKEN: 'userauth-gssapi-token', 117 | MSG_USERAUTH_GSSAPI_EXCHANGE_COMPLETE: 'userauth-gssapi-exchange-complete', 118 | MSG_USERAUTH_GSSAPI_ERROR: 'userauth-gssapi-error', 119 | MSG_USERAUTH_GSSAPI_ERRTOK: 'userauth-gssapi-error-token', 120 | MSG_USERAUTH_GSSAPI_MIC: 'userauth-gssapi-mic' 121 | } 122 | 123 | 124 | # authentication request return codes: 125 | AUTH_SUCCESSFUL, AUTH_PARTIALLY_SUCCESSFUL, AUTH_FAILED = range(3) 126 | 127 | 128 | # channel request failed reasons: 129 | (OPEN_SUCCEEDED, 130 | OPEN_FAILED_ADMINISTRATIVELY_PROHIBITED, 131 | OPEN_FAILED_CONNECT_FAILED, 132 | OPEN_FAILED_UNKNOWN_CHANNEL_TYPE, 133 | OPEN_FAILED_RESOURCE_SHORTAGE) = range(0, 5) 134 | 135 | 136 | CONNECTION_FAILED_CODE = { 137 | 1: 'Administratively prohibited', 138 | 2: 'Connect failed', 139 | 3: 'Unknown channel type', 140 | 4: 'Resource shortage' 141 | } 142 | 143 | 144 | DISCONNECT_SERVICE_NOT_AVAILABLE, DISCONNECT_AUTH_CANCELLED_BY_USER, \ 145 | DISCONNECT_NO_MORE_AUTH_METHODS_AVAILABLE = 7, 13, 14 146 | 147 | zero_byte = byte_chr(0) 148 | one_byte = byte_chr(1) 149 | four_byte = byte_chr(4) 150 | max_byte = byte_chr(0xff) 151 | cr_byte = byte_chr(13) 152 | linefeed_byte = byte_chr(10) 153 | crlf = cr_byte + linefeed_byte 154 | 155 | if PY2: 156 | cr_byte_value = cr_byte 157 | linefeed_byte_value = linefeed_byte 158 | else: 159 | cr_byte_value = 13 160 | linefeed_byte_value = 10 161 | 162 | 163 | def asbytes(s): 164 | """Coerce to bytes if possible or return unchanged.""" 165 | if isinstance(s, bytes_types): 166 | return s 167 | if isinstance(s, text_type): 168 | # Accept text and encode as utf-8 for compatibility only. 169 | return s.encode("utf-8") 170 | asbytes = getattr(s, "asbytes", None) 171 | if asbytes is not None: 172 | return asbytes() 173 | # May be an object that implements the buffer api, let callers handle. 174 | return s 175 | 176 | 177 | xffffffff = long(0xffffffff) 178 | x80000000 = long(0x80000000) 179 | o666 = 438 180 | o660 = 432 181 | o644 = 420 182 | o600 = 384 183 | o777 = 511 184 | o700 = 448 185 | o70 = 56 186 | 187 | DEBUG = logging.DEBUG 188 | INFO = logging.INFO 189 | WARNING = logging.WARNING 190 | ERROR = logging.ERROR 191 | CRITICAL = logging.CRITICAL 192 | 193 | # Common IO/select/etc sleep period, in seconds 194 | io_sleep = 0.01 195 | 196 | DEFAULT_WINDOW_SIZE = 64 * 2 ** 15 197 | DEFAULT_MAX_PACKET_SIZE = 2 ** 15 198 | 199 | # lower bound on the max packet size we'll accept from the remote host 200 | # Minimum packet size is 32768 bytes according to 201 | # http://www.ietf.org/rfc/rfc4254.txt 202 | MIN_WINDOW_SIZE = 2 ** 15 203 | 204 | # However, according to http://www.ietf.org/rfc/rfc4253.txt it is perfectly 205 | # legal to accept a size much smaller, as OpenSSH client does as size 16384. 206 | MIN_PACKET_SIZE = 2 ** 12 207 | 208 | # Max windows size according to http://www.ietf.org/rfc/rfc4254.txt 209 | MAX_WINDOW_SIZE = 2 ** 32 - 1 210 | -------------------------------------------------------------------------------- /paramiko/sftp_attr.py: -------------------------------------------------------------------------------- 1 | # Copyright (C) 2003-2006 Robey Pointer 2 | # 3 | # This file is part of paramiko. 4 | # 5 | # Paramiko is free software; you can redistribute it and/or modify it under the 6 | # terms of the GNU Lesser General Public License as published by the Free 7 | # Software Foundation; either version 2.1 of the License, or (at your option) 8 | # any later version. 9 | # 10 | # Paramiko is distributed in the hope that it will be useful, but WITHOUT ANY 11 | # WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR 12 | # A PARTICULAR PURPOSE. See the GNU Lesser General Public License for more 13 | # details. 14 | # 15 | # You should have received a copy of the GNU Lesser General Public License 16 | # along with Paramiko; if not, write to the Free Software Foundation, Inc., 17 | # 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA. 18 | 19 | import stat 20 | import time 21 | from paramiko.common import x80000000, o700, o70, xffffffff 22 | from paramiko.py3compat import long, b 23 | 24 | 25 | class SFTPAttributes (object): 26 | """ 27 | Representation of the attributes of a file (or proxied file) for SFTP in 28 | client or server mode. It attemps to mirror the object returned by 29 | `os.stat` as closely as possible, so it may have the following fields, 30 | with the same meanings as those returned by an `os.stat` object: 31 | 32 | - ``st_size`` 33 | - ``st_uid`` 34 | - ``st_gid`` 35 | - ``st_mode`` 36 | - ``st_atime`` 37 | - ``st_mtime`` 38 | 39 | Because SFTP allows flags to have other arbitrary named attributes, these 40 | are stored in a dict named ``attr``. Occasionally, the filename is also 41 | stored, in ``filename``. 42 | """ 43 | 44 | FLAG_SIZE = 1 45 | FLAG_UIDGID = 2 46 | FLAG_PERMISSIONS = 4 47 | FLAG_AMTIME = 8 48 | FLAG_EXTENDED = x80000000 49 | 50 | def __init__(self): 51 | """ 52 | Create a new (empty) SFTPAttributes object. All fields will be empty. 53 | """ 54 | self._flags = 0 55 | self.st_size = None 56 | self.st_uid = None 57 | self.st_gid = None 58 | self.st_mode = None 59 | self.st_atime = None 60 | self.st_mtime = None 61 | self.attr = {} 62 | 63 | @classmethod 64 | def from_stat(cls, obj, filename=None): 65 | """ 66 | Create an `.SFTPAttributes` object from an existing ``stat`` object (an 67 | object returned by `os.stat`). 68 | 69 | :param object obj: an object returned by `os.stat` (or equivalent). 70 | :param str filename: the filename associated with this file. 71 | :return: new `.SFTPAttributes` object with the same attribute fields. 72 | """ 73 | attr = cls() 74 | attr.st_size = obj.st_size 75 | attr.st_uid = obj.st_uid 76 | attr.st_gid = obj.st_gid 77 | attr.st_mode = obj.st_mode 78 | attr.st_atime = obj.st_atime 79 | attr.st_mtime = obj.st_mtime 80 | if filename is not None: 81 | attr.filename = filename 82 | return attr 83 | 84 | def __repr__(self): 85 | return '' % self._debug_str() 86 | 87 | # ...internals... 88 | @classmethod 89 | def _from_msg(cls, msg, filename=None, longname=None): 90 | attr = cls() 91 | attr._unpack(msg) 92 | if filename is not None: 93 | attr.filename = filename 94 | if longname is not None: 95 | attr.longname = longname 96 | return attr 97 | 98 | def _unpack(self, msg): 99 | self._flags = msg.get_int() 100 | if self._flags & self.FLAG_SIZE: 101 | self.st_size = msg.get_int64() 102 | if self._flags & self.FLAG_UIDGID: 103 | self.st_uid = msg.get_int() 104 | self.st_gid = msg.get_int() 105 | if self._flags & self.FLAG_PERMISSIONS: 106 | self.st_mode = msg.get_int() 107 | if self._flags & self.FLAG_AMTIME: 108 | self.st_atime = msg.get_int() 109 | self.st_mtime = msg.get_int() 110 | if self._flags & self.FLAG_EXTENDED: 111 | count = msg.get_int() 112 | for i in range(count): 113 | self.attr[msg.get_string()] = msg.get_string() 114 | 115 | def _pack(self, msg): 116 | self._flags = 0 117 | if self.st_size is not None: 118 | self._flags |= self.FLAG_SIZE 119 | if (self.st_uid is not None) and (self.st_gid is not None): 120 | self._flags |= self.FLAG_UIDGID 121 | if self.st_mode is not None: 122 | self._flags |= self.FLAG_PERMISSIONS 123 | if (self.st_atime is not None) and (self.st_mtime is not None): 124 | self._flags |= self.FLAG_AMTIME 125 | if len(self.attr) > 0: 126 | self._flags |= self.FLAG_EXTENDED 127 | msg.add_int(self._flags) 128 | if self._flags & self.FLAG_SIZE: 129 | msg.add_int64(self.st_size) 130 | if self._flags & self.FLAG_UIDGID: 131 | msg.add_int(self.st_uid) 132 | msg.add_int(self.st_gid) 133 | if self._flags & self.FLAG_PERMISSIONS: 134 | msg.add_int(self.st_mode) 135 | if self._flags & self.FLAG_AMTIME: 136 | # throw away any fractional seconds 137 | msg.add_int(long(self.st_atime)) 138 | msg.add_int(long(self.st_mtime)) 139 | if self._flags & self.FLAG_EXTENDED: 140 | msg.add_int(len(self.attr)) 141 | for key, val in self.attr.items(): 142 | msg.add_string(key) 143 | msg.add_string(val) 144 | return 145 | 146 | def _debug_str(self): 147 | out = '[ ' 148 | if self.st_size is not None: 149 | out += 'size=%d ' % self.st_size 150 | if (self.st_uid is not None) and (self.st_gid is not None): 151 | out += 'uid=%d gid=%d ' % (self.st_uid, self.st_gid) 152 | if self.st_mode is not None: 153 | out += 'mode=' + oct(self.st_mode) + ' ' 154 | if (self.st_atime is not None) and (self.st_mtime is not None): 155 | out += 'atime=%d mtime=%d ' % (self.st_atime, self.st_mtime) 156 | for k, v in self.attr.items(): 157 | out += '"%s"=%r ' % (str(k), v) 158 | out += ']' 159 | return out 160 | 161 | @staticmethod 162 | def _rwx(n, suid, sticky=False): 163 | if suid: 164 | suid = 2 165 | out = '-r'[n >> 2] + '-w'[(n >> 1) & 1] 166 | if sticky: 167 | out += '-xTt'[suid + (n & 1)] 168 | else: 169 | out += '-xSs'[suid + (n & 1)] 170 | return out 171 | 172 | def __str__(self): 173 | """create a unix-style long description of the file (like ls -l)""" 174 | if self.st_mode is not None: 175 | kind = stat.S_IFMT(self.st_mode) 176 | if kind == stat.S_IFIFO: 177 | ks = 'p' 178 | elif kind == stat.S_IFCHR: 179 | ks = 'c' 180 | elif kind == stat.S_IFDIR: 181 | ks = 'd' 182 | elif kind == stat.S_IFBLK: 183 | ks = 'b' 184 | elif kind == stat.S_IFREG: 185 | ks = '-' 186 | elif kind == stat.S_IFLNK: 187 | ks = 'l' 188 | elif kind == stat.S_IFSOCK: 189 | ks = 's' 190 | else: 191 | ks = '?' 192 | ks += self._rwx( 193 | (self.st_mode & o700) >> 6, self.st_mode & stat.S_ISUID) 194 | ks += self._rwx( 195 | (self.st_mode & o70) >> 3, self.st_mode & stat.S_ISGID) 196 | ks += self._rwx( 197 | self.st_mode & 7, self.st_mode & stat.S_ISVTX, True) 198 | else: 199 | ks = '?---------' 200 | # compute display date 201 | if (self.st_mtime is None) or (self.st_mtime == xffffffff): 202 | # shouldn't really happen 203 | datestr = '(unknown date)' 204 | else: 205 | if abs(time.time() - self.st_mtime) > 15552000: 206 | # (15552000 = 6 months) 207 | datestr = time.strftime( 208 | '%d %b %Y', time.localtime(self.st_mtime)) 209 | else: 210 | datestr = time.strftime( 211 | '%d %b %H:%M', time.localtime(self.st_mtime)) 212 | filename = getattr(self, 'filename', '?') 213 | 214 | # not all servers support uid/gid 215 | uid = self.st_uid 216 | gid = self.st_gid 217 | size = self.st_size 218 | if uid is None: 219 | uid = 0 220 | if gid is None: 221 | gid = 0 222 | if size is None: 223 | size = 0 224 | 225 | return '%s 1 %-8d %-8d %8d %-12s %s' % ( 226 | ks, uid, gid, size, datestr, filename) 227 | 228 | def asbytes(self): 229 | return b(str(self)) 230 | -------------------------------------------------------------------------------- /paramiko/dsskey.py: -------------------------------------------------------------------------------- 1 | # Copyright (C) 2003-2007 Robey Pointer 2 | # 3 | # This file is part of paramiko. 4 | # 5 | # Paramiko is free software; you can redistribute it and/or modify it under the 6 | # terms of the GNU Lesser General Public License as published by the Free 7 | # Software Foundation; either version 2.1 of the License, or (at your option) 8 | # any later version. 9 | # 10 | # Paramiko is distributed in the hope that it will be useful, but WITHOUT ANY 11 | # WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR 12 | # A PARTICULAR PURPOSE. See the GNU Lesser General Public License for more 13 | # details. 14 | # 15 | # You should have received a copy of the GNU Lesser General Public License 16 | # along with Paramiko; if not, write to the Free Software Foundation, Inc., 17 | # 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA. 18 | 19 | """ 20 | DSS keys. 21 | """ 22 | 23 | from cryptography.exceptions import InvalidSignature 24 | from cryptography.hazmat.backends import default_backend 25 | from cryptography.hazmat.primitives import hashes, serialization 26 | from cryptography.hazmat.primitives.asymmetric import dsa 27 | from cryptography.hazmat.primitives.asymmetric.utils import ( 28 | decode_dss_signature, encode_dss_signature 29 | ) 30 | 31 | from paramiko import util 32 | from paramiko.common import zero_byte 33 | from paramiko.ssh_exception import SSHException 34 | from paramiko.message import Message 35 | from paramiko.ber import BER, BERException 36 | from paramiko.pkey import PKey 37 | 38 | 39 | class DSSKey(PKey): 40 | """ 41 | Representation of a DSS key which can be used to sign an verify SSH2 42 | data. 43 | """ 44 | 45 | def __init__(self, msg=None, data=None, filename=None, password=None, 46 | vals=None, file_obj=None): 47 | self.p = None 48 | self.q = None 49 | self.g = None 50 | self.y = None 51 | self.x = None 52 | if file_obj is not None: 53 | self._from_private_key(file_obj, password) 54 | return 55 | if filename is not None: 56 | self._from_private_key_file(filename, password) 57 | return 58 | if (msg is None) and (data is not None): 59 | msg = Message(data) 60 | if vals is not None: 61 | self.p, self.q, self.g, self.y = vals 62 | else: 63 | if msg is None: 64 | raise SSHException('Key object may not be empty') 65 | if msg.get_text() != 'ssh-dss': 66 | raise SSHException('Invalid key') 67 | self.p = msg.get_mpint() 68 | self.q = msg.get_mpint() 69 | self.g = msg.get_mpint() 70 | self.y = msg.get_mpint() 71 | self.size = util.bit_length(self.p) 72 | 73 | def asbytes(self): 74 | m = Message() 75 | m.add_string('ssh-dss') 76 | m.add_mpint(self.p) 77 | m.add_mpint(self.q) 78 | m.add_mpint(self.g) 79 | m.add_mpint(self.y) 80 | return m.asbytes() 81 | 82 | def __str__(self): 83 | return self.asbytes() 84 | 85 | def __hash__(self): 86 | return hash((self.get_name(), self.p, self.q, self.g, self.y)) 87 | 88 | def get_name(self): 89 | return 'ssh-dss' 90 | 91 | def get_bits(self): 92 | return self.size 93 | 94 | def can_sign(self): 95 | return self.x is not None 96 | 97 | def sign_ssh_data(self, data): 98 | key = dsa.DSAPrivateNumbers( 99 | x=self.x, 100 | public_numbers=dsa.DSAPublicNumbers( 101 | y=self.y, 102 | parameter_numbers=dsa.DSAParameterNumbers( 103 | p=self.p, 104 | q=self.q, 105 | g=self.g 106 | ) 107 | ) 108 | ).private_key(backend=default_backend()) 109 | signer = key.signer(hashes.SHA1()) 110 | signer.update(data) 111 | r, s = decode_dss_signature(signer.finalize()) 112 | 113 | m = Message() 114 | m.add_string('ssh-dss') 115 | # apparently, in rare cases, r or s may be shorter than 20 bytes! 116 | rstr = util.deflate_long(r, 0) 117 | sstr = util.deflate_long(s, 0) 118 | if len(rstr) < 20: 119 | rstr = zero_byte * (20 - len(rstr)) + rstr 120 | if len(sstr) < 20: 121 | sstr = zero_byte * (20 - len(sstr)) + sstr 122 | m.add_string(rstr + sstr) 123 | return m 124 | 125 | def verify_ssh_sig(self, data, msg): 126 | if len(msg.asbytes()) == 40: 127 | # spies.com bug: signature has no header 128 | sig = msg.asbytes() 129 | else: 130 | kind = msg.get_text() 131 | if kind != 'ssh-dss': 132 | return 0 133 | sig = msg.get_binary() 134 | 135 | # pull out (r, s) which are NOT encoded as mpints 136 | sigR = util.inflate_long(sig[:20], 1) 137 | sigS = util.inflate_long(sig[20:], 1) 138 | 139 | signature = encode_dss_signature(sigR, sigS) 140 | 141 | key = dsa.DSAPublicNumbers( 142 | y=self.y, 143 | parameter_numbers=dsa.DSAParameterNumbers( 144 | p=self.p, 145 | q=self.q, 146 | g=self.g 147 | ) 148 | ).public_key(backend=default_backend()) 149 | verifier = key.verifier(signature, hashes.SHA1()) 150 | verifier.update(data) 151 | try: 152 | verifier.verify() 153 | except InvalidSignature: 154 | return False 155 | else: 156 | return True 157 | 158 | def write_private_key_file(self, filename, password=None): 159 | key = dsa.DSAPrivateNumbers( 160 | x=self.x, 161 | public_numbers=dsa.DSAPublicNumbers( 162 | y=self.y, 163 | parameter_numbers=dsa.DSAParameterNumbers( 164 | p=self.p, 165 | q=self.q, 166 | g=self.g 167 | ) 168 | ) 169 | ).private_key(backend=default_backend()) 170 | 171 | self._write_private_key_file( 172 | filename, 173 | key, 174 | serialization.PrivateFormat.TraditionalOpenSSL, 175 | password=password 176 | ) 177 | 178 | def write_private_key(self, file_obj, password=None): 179 | key = dsa.DSAPrivateNumbers( 180 | x=self.x, 181 | public_numbers=dsa.DSAPublicNumbers( 182 | y=self.y, 183 | parameter_numbers=dsa.DSAParameterNumbers( 184 | p=self.p, 185 | q=self.q, 186 | g=self.g 187 | ) 188 | ) 189 | ).private_key(backend=default_backend()) 190 | 191 | self._write_private_key( 192 | file_obj, 193 | key, 194 | serialization.PrivateFormat.TraditionalOpenSSL, 195 | password=password 196 | ) 197 | 198 | @staticmethod 199 | def generate(bits=1024, progress_func=None): 200 | """ 201 | Generate a new private DSS key. This factory function can be used to 202 | generate a new host key or authentication key. 203 | 204 | :param int bits: number of bits the generated key should be. 205 | :param progress_func: Unused 206 | :return: new `.DSSKey` private key 207 | """ 208 | numbers = dsa.generate_private_key( 209 | bits, backend=default_backend() 210 | ).private_numbers() 211 | key = DSSKey(vals=( 212 | numbers.public_numbers.parameter_numbers.p, 213 | numbers.public_numbers.parameter_numbers.q, 214 | numbers.public_numbers.parameter_numbers.g, 215 | numbers.public_numbers.y 216 | )) 217 | key.x = numbers.x 218 | return key 219 | 220 | # ...internals... 221 | 222 | def _from_private_key_file(self, filename, password): 223 | data = self._read_private_key_file('DSA', filename, password) 224 | self._decode_key(data) 225 | 226 | def _from_private_key(self, file_obj, password): 227 | data = self._read_private_key('DSA', file_obj, password) 228 | self._decode_key(data) 229 | 230 | def _decode_key(self, data): 231 | # private key file contains: 232 | # DSAPrivateKey = { version = 0, p, q, g, y, x } 233 | try: 234 | keylist = BER(data).decode() 235 | except BERException as e: 236 | raise SSHException('Unable to parse key file: ' + str(e)) 237 | if ( 238 | type(keylist) is not list or 239 | len(keylist) < 6 or 240 | keylist[0] != 0 241 | ): 242 | raise SSHException( 243 | 'not a valid DSA private key file (bad ber encoding)') 244 | self.p = keylist[1] 245 | self.q = keylist[2] 246 | self.g = keylist[3] 247 | self.y = keylist[4] 248 | self.x = keylist[5] 249 | self.size = util.bit_length(self.p) 250 | -------------------------------------------------------------------------------- /paramiko/util.py: -------------------------------------------------------------------------------- 1 | # Copyright (C) 2003-2007 Robey Pointer 2 | # 3 | # This file is part of paramiko. 4 | # 5 | # Paramiko is free software; you can redistribute it and/or modify it under the 6 | # terms of the GNU Lesser General Public License as published by the Free 7 | # Software Foundation; either version 2.1 of the License, or (at your option) 8 | # any later version. 9 | # 10 | # Paramiko is distributed in the hope that it will be useful, but WITHOUT ANY 11 | # WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR 12 | # A PARTICULAR PURPOSE. See the GNU Lesser General Public License for more 13 | # details. 14 | # 15 | # You should have received a copy of the GNU Lesser General Public License 16 | # along with Paramiko; if not, write to the Free Software Foundation, Inc., 17 | # 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA. 18 | 19 | """ 20 | Useful functions used by the rest of paramiko. 21 | """ 22 | 23 | from __future__ import generators 24 | 25 | import errno 26 | import sys 27 | import struct 28 | import traceback 29 | import threading 30 | import logging 31 | 32 | from paramiko.common import DEBUG, zero_byte, xffffffff, max_byte 33 | from paramiko.py3compat import PY2, long, byte_chr, byte_ord, b 34 | from paramiko.config import SSHConfig 35 | 36 | 37 | def inflate_long(s, always_positive=False): 38 | """turns a normalized byte string into a long-int 39 | (adapted from Crypto.Util.number)""" 40 | out = long(0) 41 | negative = 0 42 | if not always_positive and (len(s) > 0) and (byte_ord(s[0]) >= 0x80): 43 | negative = 1 44 | if len(s) % 4: 45 | filler = zero_byte 46 | if negative: 47 | filler = max_byte 48 | # never convert this to ``s +=`` because this is a string, not a number 49 | # noinspection PyAugmentAssignment 50 | s = filler * (4 - len(s) % 4) + s 51 | for i in range(0, len(s), 4): 52 | out = (out << 32) + struct.unpack('>I', s[i:i + 4])[0] 53 | if negative: 54 | out -= (long(1) << (8 * len(s))) 55 | return out 56 | 57 | 58 | deflate_zero = zero_byte if PY2 else 0 59 | deflate_ff = max_byte if PY2 else 0xff 60 | 61 | 62 | def deflate_long(n, add_sign_padding=True): 63 | """turns a long-int into a normalized byte string 64 | (adapted from Crypto.Util.number)""" 65 | # after much testing, this algorithm was deemed to be the fastest 66 | s = bytes() 67 | n = long(n) 68 | while (n != 0) and (n != -1): 69 | s = struct.pack('>I', n & xffffffff) + s 70 | n >>= 32 71 | # strip off leading zeros, FFs 72 | for i in enumerate(s): 73 | if (n == 0) and (i[1] != deflate_zero): 74 | break 75 | if (n == -1) and (i[1] != deflate_ff): 76 | break 77 | else: 78 | # degenerate case, n was either 0 or -1 79 | i = (0,) 80 | if n == 0: 81 | s = zero_byte 82 | else: 83 | s = max_byte 84 | s = s[i[0]:] 85 | if add_sign_padding: 86 | if (n == 0) and (byte_ord(s[0]) >= 0x80): 87 | s = zero_byte + s 88 | if (n == -1) and (byte_ord(s[0]) < 0x80): 89 | s = max_byte + s 90 | return s 91 | 92 | 93 | def format_binary(data, prefix=''): 94 | x = 0 95 | out = [] 96 | while len(data) > x + 16: 97 | out.append(format_binary_line(data[x:x + 16])) 98 | x += 16 99 | if x < len(data): 100 | out.append(format_binary_line(data[x:])) 101 | return [prefix + line for line in out] 102 | 103 | 104 | def format_binary_line(data): 105 | left = ' '.join(['%02X' % byte_ord(c) for c in data]) 106 | right = ''.join([('.%c..' % c)[(byte_ord(c) + 63) // 95] for c in data]) 107 | return '%-50s %s' % (left, right) 108 | 109 | 110 | def safe_string(s): 111 | out = b('') 112 | for c in s: 113 | i = byte_ord(c) 114 | if 32 <= i <= 127: 115 | out += byte_chr(i) 116 | else: 117 | out += b('%%%02X' % i) 118 | return out 119 | 120 | 121 | def bit_length(n): 122 | try: 123 | return n.bit_length() 124 | except AttributeError: 125 | norm = deflate_long(n, False) 126 | hbyte = byte_ord(norm[0]) 127 | if hbyte == 0: 128 | return 1 129 | bitlen = len(norm) * 8 130 | while not (hbyte & 0x80): 131 | hbyte <<= 1 132 | bitlen -= 1 133 | return bitlen 134 | 135 | 136 | def tb_strings(): 137 | return ''.join(traceback.format_exception(*sys.exc_info())).split('\n') 138 | 139 | 140 | def generate_key_bytes(hash_alg, salt, key, nbytes): 141 | """ 142 | Given a password, passphrase, or other human-source key, scramble it 143 | through a secure hash into some keyworthy bytes. This specific algorithm 144 | is used for encrypting/decrypting private key files. 145 | 146 | :param function hash_alg: A function which creates a new hash object, such 147 | as ``hashlib.sha256``. 148 | :param salt: data to salt the hash with. 149 | :type salt: byte string 150 | :param str key: human-entered password or passphrase. 151 | :param int nbytes: number of bytes to generate. 152 | :return: Key data `str` 153 | """ 154 | keydata = bytes() 155 | digest = bytes() 156 | if len(salt) > 8: 157 | salt = salt[:8] 158 | while nbytes > 0: 159 | hash_obj = hash_alg() 160 | if len(digest) > 0: 161 | hash_obj.update(digest) 162 | hash_obj.update(b(key)) 163 | hash_obj.update(salt) 164 | digest = hash_obj.digest() 165 | size = min(nbytes, len(digest)) 166 | keydata += digest[:size] 167 | nbytes -= size 168 | return keydata 169 | 170 | 171 | def load_host_keys(filename): 172 | """ 173 | Read a file of known SSH host keys, in the format used by openssh, and 174 | return a compound dict of ``hostname -> keytype ->`` `PKey 175 | `. The hostname may be an IP address or DNS name. The 176 | keytype will be either ``"ssh-rsa"`` or ``"ssh-dss"``. 177 | 178 | This type of file unfortunately doesn't exist on Windows, but on posix, 179 | it will usually be stored in ``os.path.expanduser("~/.ssh/known_hosts")``. 180 | 181 | Since 1.5.3, this is just a wrapper around `.HostKeys`. 182 | 183 | :param str filename: name of the file to read host keys from 184 | :return: 185 | nested dict of `.PKey` objects, indexed by hostname and then keytype 186 | """ 187 | from paramiko.hostkeys import HostKeys 188 | return HostKeys(filename) 189 | 190 | 191 | def parse_ssh_config(file_obj): 192 | """ 193 | Provided only as a backward-compatible wrapper around `.SSHConfig`. 194 | """ 195 | config = SSHConfig() 196 | config.parse(file_obj) 197 | return config 198 | 199 | 200 | def lookup_ssh_host_config(hostname, config): 201 | """ 202 | Provided only as a backward-compatible wrapper around `.SSHConfig`. 203 | """ 204 | return config.lookup(hostname) 205 | 206 | 207 | def mod_inverse(x, m): 208 | # it's crazy how small Python can make this function. 209 | u1, u2, u3 = 1, 0, m 210 | v1, v2, v3 = 0, 1, x 211 | 212 | while v3 > 0: 213 | q = u3 // v3 214 | u1, v1 = v1, u1 - v1 * q 215 | u2, v2 = v2, u2 - v2 * q 216 | u3, v3 = v3, u3 - v3 * q 217 | if u2 < 0: 218 | u2 += m 219 | return u2 220 | 221 | 222 | _g_thread_ids = {} 223 | _g_thread_counter = 0 224 | _g_thread_lock = threading.Lock() 225 | 226 | 227 | def get_thread_id(): 228 | global _g_thread_ids, _g_thread_counter, _g_thread_lock 229 | tid = id(threading.currentThread()) 230 | try: 231 | return _g_thread_ids[tid] 232 | except KeyError: 233 | _g_thread_lock.acquire() 234 | try: 235 | _g_thread_counter += 1 236 | ret = _g_thread_ids[tid] = _g_thread_counter 237 | finally: 238 | _g_thread_lock.release() 239 | return ret 240 | 241 | 242 | def log_to_file(filename, level=DEBUG): 243 | """send paramiko logs to a logfile, 244 | if they're not already going somewhere""" 245 | l = logging.getLogger("paramiko") 246 | if len(l.handlers) > 0: 247 | return 248 | l.setLevel(level) 249 | f = open(filename, 'a') 250 | lh = logging.StreamHandler(f) 251 | frm = '%(levelname)-.3s [%(asctime)s.%(msecs)03d] thr=%(_threadid)-3d %(name)s: %(message)s' # noqa 252 | lh.setFormatter(logging.Formatter(frm, '%Y%m%d-%H:%M:%S')) 253 | l.addHandler(lh) 254 | 255 | 256 | # make only one filter object, so it doesn't get applied more than once 257 | class PFilter (object): 258 | def filter(self, record): 259 | record._threadid = get_thread_id() 260 | return True 261 | 262 | 263 | _pfilter = PFilter() 264 | 265 | 266 | def get_logger(name): 267 | l = logging.getLogger(name) 268 | l.addFilter(_pfilter) 269 | return l 270 | 271 | 272 | def retry_on_signal(function): 273 | """Retries function until it doesn't raise an EINTR error""" 274 | while True: 275 | try: 276 | return function() 277 | except EnvironmentError as e: 278 | if e.errno != errno.EINTR: 279 | raise 280 | 281 | 282 | def constant_time_bytes_eq(a, b): 283 | if len(a) != len(b): 284 | return False 285 | res = 0 286 | # noinspection PyUnresolvedReferences 287 | for i in (xrange if PY2 else range)(len(a)): # noqa: F821 288 | res |= byte_ord(a[i]) ^ byte_ord(b[i]) 289 | return res == 0 290 | 291 | 292 | class ClosingContextManager(object): 293 | def __enter__(self): 294 | return self 295 | 296 | def __exit__(self, type, value, traceback): 297 | self.close() 298 | 299 | 300 | def clamp_value(minimum, val, maximum): 301 | return max(minimum, min(val, maximum)) 302 | -------------------------------------------------------------------------------- /wetland/server/sshServer.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | import socket 3 | import threading 4 | 5 | import paramiko 6 | 7 | from wetland import config 8 | from wetland import services 9 | from wetland.output import output 10 | 11 | 12 | class ssh_server(paramiko.ServerInterface): 13 | 14 | def __init__(self, transport, blacklist, whitelist, sessionuid): 15 | self.cfg = config.cfg 16 | self.whitelist = whitelist 17 | self.blacklist = blacklist 18 | self.sessionuid = sessionuid 19 | 20 | # init hacker's transport 21 | self.hacker_trans = transport 22 | self.hacker_ip, self.hacker_port = transport.getpeername() 23 | 24 | self.opt = output(self) 25 | 26 | self.docker_host = self.cfg.get("wetland", "docker_addr") 27 | self.docker_port = self.cfg.getint("wetland", "docker_port") 28 | 29 | # bind docker' socket on fake ip 30 | sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM) 31 | sock.connect((self.docker_host, self.docker_port)) 32 | 33 | # init docker's transport with socket 34 | self.docker_trans = paramiko.Transport(sock) 35 | self.docker_trans.start_client() 36 | 37 | # {hacker channel : docker channel} 38 | self.chain = {} 39 | 40 | # check auth 41 | def get_allowed_auths(self, username): 42 | return 'password' 43 | 44 | def check_auth_password(self, username, password): 45 | 46 | # whitelist mode and hacker havent come in 47 | if self.whitelist is not None and not self.blacklist[-1]: 48 | self.opt.o('content', 'pwd', ":".join((username, password))) 49 | if self.hacker_ip not in self.whitelist: 50 | return paramiko.AUTH_FAILED 51 | else: 52 | self.docker_trans.auth_password(username='root', 53 | password='wetlandrootwetland') 54 | s = self.docker_trans.open_session() 55 | s.exec_command('useradd -m %s' % username) 56 | s.close() 57 | s = self.docker_trans.open_session() 58 | s.exec_command('echo "%s:%s" | chpasswd' % (username, password)) 59 | s.close() 60 | self.blacklist[-1] = self.hacker_ip 61 | self.opt.o('wetland', 'login_successful', 62 | ":".join((username, password))) 63 | return paramiko.AUTH_SUCCESSFUL 64 | 65 | elif self.cfg.getboolean('wetland', 'blacklist') and self.blacklist[-1]: 66 | print self.blacklist 67 | if self.hacker_ip != self.blacklist[-1] and \ 68 | self.blacklist[self.hacker_ip] > 3: 69 | self.opt.o('content', 'pwd', ":".join((username, password))) 70 | return paramiko.AUTH_FAILED 71 | 72 | try: 73 | # redirect all auth request to sshd container 74 | self.docker_trans.auth_password(username=username, 75 | password=password) 76 | except Exception: 77 | self.blacklist[self.hacker_ip] += 1 78 | self.opt.o('content', 'pwd', ":".join((username, password))) 79 | return paramiko.AUTH_FAILED 80 | else: 81 | self.opt.o('wetland', 'login_successful', 82 | ":".join((username, password))) 83 | return paramiko.AUTH_SUCCESSFUL 84 | 85 | self.blacklist[self.hacker_ip] += 1 86 | 87 | # redirect all auth request to sshd container 88 | self.docker_trans.auth_password(username=username, 89 | password=password) 90 | else: 91 | self.opt.o('content', 'pwd', ":".join((username, password))) 92 | try: 93 | # redirect all auth request to sshd container 94 | self.docker_trans.auth_password(username=username, 95 | password=password) 96 | except Exception: 97 | return paramiko.AUTH_FAILED 98 | else: 99 | self.opt.o('wetland', 'login_successful', 100 | ":".join((username, password))) 101 | self.blacklist[-1] = self.hacker_ip 102 | return paramiko.AUTH_SUCCESSFUL 103 | 104 | def check_auth_publickey(self, username, key): 105 | return paramiko.AUTH_FAILED 106 | 107 | # check the kind of channel can be opened 108 | def check_channel_request(self, kind, chanid): 109 | self.opt.o('wetland', 'channel_request', kind) 110 | return paramiko.OPEN_SUCCEEDED 111 | 112 | def check_global_request(self, kind, msg): 113 | self.opt.o('wetland', 'global_request', str(msg)) 114 | return True 115 | 116 | def check_channel_pty_request(self, channel, term, width, height, 117 | pixelwidth, pixelheight, modes): 118 | try: 119 | docker_session = self.docker_trans.open_session() 120 | docker_session.get_pty() 121 | self.chain[channel.get_id()] = docker_session.get_id() 122 | except Exception: 123 | self.opt.o('wetland', 'pty_request', "failed") 124 | return False 125 | else: 126 | self.opt.o('wetland', 'pty_request', "success") 127 | return True 128 | 129 | def check_channel_shell_request(self, hacker_session): 130 | try: 131 | docker_id = self.chain[hacker_session.get_id()] 132 | docker_session = self.docker_trans._channels.get(docker_id) 133 | except Exception: 134 | docker_session = self.docker_trans.open_session() 135 | docker_session.get_pty() 136 | self.chain[hacker_session.get_id()] = docker_session.get_id() 137 | 138 | try: 139 | docker_session.invoke_shell() 140 | 141 | service_thread = threading.Thread(target=services.shell_service, 142 | args=(hacker_session, 143 | docker_session, 144 | self.opt)) 145 | service_thread.setDaemon(True) 146 | service_thread.start() 147 | 148 | except Exception, e: 149 | print e 150 | self.opt.o('wetland', 'shell_request', "failed") 151 | return False 152 | else: 153 | self.opt.o('wetland', 'shell_request', "success") 154 | return True 155 | 156 | def check_channel_exec_request(self, hacker_session, command): 157 | 158 | try: 159 | docker_session = self.docker_trans.open_session() 160 | self.chain[hacker_session.get_id()] = docker_session.get_id() 161 | service_thread = threading.Thread(target=services.exec_service, 162 | args=(hacker_session, 163 | docker_session, 164 | command, 165 | self.opt)) 166 | service_thread.setDaemon(True) 167 | service_thread.start() 168 | except Exception: 169 | self.opt.o('wetland', 'exec_request', "failed") 170 | return False 171 | else: 172 | self.opt.o('wetland', 'exec_request', "success") 173 | return True 174 | 175 | # check for reverse forward channel 176 | def check_port_forward_request(self, address, port): 177 | def handler(chann, ori, dest): 178 | services.reverse_handler(chann, ori, dest, self.hacker_trans, 179 | self.opt) 180 | 181 | flag = self.docker_trans.request_port_forward(address, port, 182 | handler=handler) 183 | tmp = "success" if flag else 'failed' 184 | self.opt.o('wetland', 'reverse_request', 185 | ', '.join([tmp, address, str(port)])) 186 | 187 | return flag 188 | 189 | def check_channel_forward_agent_request(self, channel): 190 | self.opt.o('wetland', 'agent_request', 'failed') 191 | return False 192 | 193 | def check_channel_env_request(self, channel, name, value): 194 | try: 195 | docker_id = self.chain[channel.get_id()] 196 | docker_session = self.docker_trans._channels.get(docker_id) 197 | docker_session.set_environment_variable(name, value) 198 | except Exception: 199 | self.opt.o('wetland', 'env_request', 'failed') 200 | return False 201 | else: 202 | self.opt.o('wetland', 'env_request', 'success') 203 | return True 204 | 205 | def check_channel_direct_tcpip_request(self, chanid, origin, destination): 206 | try: 207 | docker_channel = self.docker_trans.open_channel('direct-tcpip', 208 | dest_addr=destination, 209 | src_addr=origin) 210 | self.chain[chanid] = docker_channel.get_id() 211 | 212 | except paramiko.ChannelException: 213 | self.opt.o('wetland', 'direct_request', 'failed') 214 | return paramiko.OPEN_FAILED_CONNECT_FAILED 215 | else: 216 | self.opt.o('wetland', 'direct_request', 217 | 'ori:%s, dest:%s' % (origin, destination)) 218 | service_thread = threading.Thread(target=services.direct_service, 219 | args=(chanid, 220 | self.hacker_trans, 221 | docker_channel, 222 | self.opt)) 223 | service_thread.setDaemon(True) 224 | service_thread.start() 225 | return paramiko.OPEN_SUCCEEDED 226 | -------------------------------------------------------------------------------- /util/playlog.py: -------------------------------------------------------------------------------- 1 | import os 2 | import re 3 | import sys 4 | import argparse 5 | from collections import deque 6 | 7 | 8 | class shell_player(object): 9 | 10 | def __init__(self, filename): 11 | self.filename = filename 12 | 13 | def get(self): 14 | shell_sessions = [] 15 | with open(self.filename) as log_file: 16 | datas = [] 17 | for i in log_file.xreadlines(): 18 | a = i.strip().split(" ") 19 | 20 | timestamp = a[0] 21 | data = a[1] 22 | if ':' in data: 23 | data = data.split(":") 24 | datas.append((timestamp, data[0], data[1].decode("hex"))) 25 | elif data == 'NNNNNNNNNNNNNNNNNNNN': 26 | if datas: 27 | shell_sessions.append(datas) 28 | datas = [] 29 | shell_sessions.append(datas) 30 | self.sessions = shell_sessions 31 | 32 | def play(self, datas): 33 | startstamp = datas[0][0] 34 | endstamp = datas[-1][0] 35 | 36 | print """ 37 | ============================================================ 38 | ========= Session start at %s ========= 39 | ========= Session close at %s ========= 40 | ============================================================ 41 | """ % (startstamp, endstamp) 42 | 43 | for data in datas: 44 | direction = data[1] 45 | text = data[2] 46 | 47 | if direction == '[V]': 48 | sys.stdout.write(text.lstrip('\r\n')) 49 | 50 | elif direction == '[H]' and text == '\r': 51 | raw_input() 52 | print """ 53 | ================================================= 54 | ============= Session closed ==================== 55 | ================================================= """ 56 | def start(self): 57 | self.get() 58 | 59 | print '[+] Detect %d sessions' % len(self.sessions) 60 | 61 | for n, i in enumerate(self.sessions): 62 | print '[%d] %s =====> %s' % (n, self.sessions[n][0][0], 63 | self.sessions[n][-1][0]) 64 | 65 | try: 66 | index = int(raw_input("\n[+] which one do you want to replay? ")) 67 | os.system("clear") 68 | self.play(self.sessions[index]) 69 | except Exception, e: 70 | print e 71 | 72 | 73 | class exec_player(object): 74 | def __init__(self, filename, save_path): 75 | self.filename = filename 76 | self.save_path = save_path 77 | 78 | def get(self): 79 | exec_sessions = [] 80 | with open(self.filename) as log_file: 81 | datas = [] 82 | for i in log_file.xreadlines(): 83 | a = i.strip().split(" ") 84 | 85 | timestamp = a[0] 86 | data = a[1] 87 | if ':' in data: 88 | data = data.split(":") 89 | datas.append((timestamp, data[0], data[1].decode("hex"))) 90 | elif data == 'NNNNNNNNNNNNNNNNNNNN': 91 | if datas: 92 | if re.match('^scp\s*(-r)?\s*-[ft]\s*\S*$', datas[0][2]): 93 | self.scp(datas) 94 | datas = [] 95 | else: 96 | exec_sessions.append(datas) 97 | datas = [] 98 | 99 | if re.match('^scp\s*(-r)?\s*-[ft]\s*\S*$', datas[0][2]): 100 | self.scp(datas) 101 | else: 102 | exec_sessions.append(datas) 103 | self.sessions = exec_sessions 104 | 105 | def start(self): 106 | self.get() 107 | print '[+] Detect %d sessions' % len(self.sessions) 108 | 109 | for n, session in enumerate(self.sessions): 110 | print '[%d] %s =====> %s' % (n, self.sessions[n][0][0], 111 | self.sessions[n][-1][0]) 112 | for s in session: 113 | if s[1] == '[H]': 114 | print '--------------------------- Hacker send----------------------------' 115 | if s[1] == '[V]': 116 | print '--------------------------- Docker send----------------------------' 117 | print s[2] 118 | 119 | def scp(self, datas): 120 | t = re.split('\s+', datas[0][2]) 121 | datas.remove(datas[0]) 122 | datas.remove(datas[0]) 123 | dq = deque() 124 | 125 | if '-f' in t: 126 | if '-r' in t: 127 | print '[+] Detect scp download folder at ===> %s' % t[-1] 128 | else: 129 | print '[+] Detect scp upload file at ===> %s' % t[-1] 130 | dq = deque([i[2] for i in datas if i[1]=='[V]']) 131 | elif '-t' in t: 132 | if '-r' in t: 133 | print '[+] Detect scp upload folder at ===> %s' % t[-1] 134 | else: 135 | print '[+] Detect scp download file at ===> %s' % t[-1] 136 | dq = deque([i[2] for i in datas if i[1]=='[H]']) 137 | else: 138 | print '[-] scp protocol invalid' 139 | 140 | f = self.extract_files(dq) 141 | if f: 142 | self.save_files(f, self.save_path) 143 | 144 | def extract_files(self, datas): 145 | 146 | f = {} 147 | while datas and datas[0] != 'E\n': 148 | hdr = datas.popleft() 149 | name = hdr.strip().split(' ')[2] 150 | length = int(hdr.strip().split(' ')[1]) 151 | 152 | if re.match('^D\d{4}\s+\d+\s+\S+\n$', hdr): 153 | sub_dict = self.extract_files(datas) 154 | if sub_dict is None: 155 | return None 156 | f[name] = sub_dict 157 | tail = datas.popleft() 158 | if tail != 'E\n': 159 | print '[-] scp tail error' 160 | return None 161 | 162 | elif re.match('^C\d{4}\s+\d+\s+\S+\n$', hdr): 163 | contents = [] 164 | while True: 165 | tmp = datas.popleft() 166 | if tmp == '\x00': 167 | break 168 | 169 | if tmp.endswith('\x00') and \ 170 | (len(''.join(contents)) + len(tmp) - 1 == length): 171 | contents.append(tmp[:-1]) 172 | break 173 | 174 | contents.append(tmp) 175 | f[name] = ''.join(contents) 176 | else: 177 | print '[-] scp head error' 178 | return None 179 | break 180 | return f 181 | 182 | def save_files(self, file_dict, root_path): 183 | if not os.path.exists(root_path): 184 | os.makedirs(root_path) 185 | 186 | for name, content in file_dict.items(): 187 | name = re.sub('[^A-Za-z0-9]', '-', name) 188 | new_path = os.path.join(root_path, name) 189 | 190 | if isinstance(content, str): 191 | with open(new_path, 'wb') as f: 192 | f.write(content) 193 | 194 | elif isinstance(content, dict): 195 | self.save_files(file_dict[name], os.path.join(root_path, name)) 196 | 197 | else: 198 | return None 199 | 200 | 201 | class forward_player(object): 202 | def __init__(self, filename): 203 | self.filename = filename 204 | 205 | def start(self): 206 | self.get() 207 | print '[+] Detect %d sessions' % len(self.sessions) 208 | for session in self.sessions: 209 | print '\n\n================================ New session ==============================' 210 | print '======= %s -------> %s ==========' % (session[0][0], session[-1][0]) 211 | print '===========================================================================' 212 | for s in session: 213 | if s[1] == '[H]': 214 | print '----------------------------- Hacker send------------------------------' 215 | if s[1] == '[V]': 216 | print '----------------------------- Docker send------------------------------' 217 | print s[2] 218 | 219 | def get(self): 220 | forward_sessions = [] 221 | with open(self.filename) as log_file: 222 | datas = [] 223 | for i in log_file.xreadlines(): 224 | a = i.strip().split(" ") 225 | 226 | timestamp = a[0] 227 | data = a[1] 228 | if ':' in data: 229 | data = data.split(":") 230 | datas.append((timestamp, data[0], data[1].decode("hex"))) 231 | elif data == 'NNNNNNNNNNNNNNNNNNNN': 232 | if datas: 233 | forward_sessions.append(datas) 234 | datas = [] 235 | 236 | forward_sessions.append(datas) 237 | self.sessions = forward_sessions 238 | 239 | 240 | if __name__ == '__main__': 241 | parser = argparse.ArgumentParser(description='Replay the logs of wetlan') 242 | parser.add_argument('-t', choices=['shell', 'exec', 'forward'], 243 | default='filetype', help='Type of the log file') 244 | parser.add_argument("p", help='The path of log file') 245 | parser.add_argument("-s", default='../download/scp', 246 | help='Extract file to this folder in exec logs') 247 | args = parser.parse_args() 248 | 249 | if args.t == 'filetype': 250 | t = os.path.splitext(os.path.split(args.p)[1])[0] 251 | if t not in ['shell', 'exec', 'direct', 'reverse']: 252 | print '[-] filename unknow or you should specify arguemnt -t' 253 | sys.exit(0) 254 | args.t = t 255 | 256 | if args.t == 'shell': 257 | a = shell_player(args.p) 258 | elif args.t == 'exec': 259 | a = exec_player(args.p, args.s) 260 | elif args.t in ['forward', 'direct', 'reverse']: 261 | a = forward_player(args.p) 262 | a.start() 263 | -------------------------------------------------------------------------------- /paramiko/message.py: -------------------------------------------------------------------------------- 1 | # Copyright (C) 2003-2007 Robey Pointer 2 | # 3 | # This file is part of paramiko. 4 | # 5 | # Paramiko is free software; you can redistribute it and/or modify it under the 6 | # terms of the GNU Lesser General Public License as published by the Free 7 | # Software Foundation; either version 2.1 of the License, or (at your option) 8 | # any later version. 9 | # 10 | # Paramiko is distributed in the hope that it will be useful, but WITHOUT ANY 11 | # WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR 12 | # A PARTICULAR PURPOSE. See the GNU Lesser General Public License for more 13 | # details. 14 | # 15 | # You should have received a copy of the GNU Lesser General Public License 16 | # along with Paramiko; if not, write to the Free Software Foundation, Inc., 17 | # 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA. 18 | 19 | """ 20 | Implementation of an SSH2 "message". 21 | """ 22 | 23 | import struct 24 | 25 | from paramiko import util 26 | from paramiko.common import zero_byte, max_byte, one_byte, asbytes 27 | from paramiko.py3compat import long, BytesIO, u, integer_types 28 | 29 | 30 | class Message (object): 31 | """ 32 | An SSH2 message is a stream of bytes that encodes some combination of 33 | strings, integers, bools, and infinite-precision integers (known in Python 34 | as longs). This class builds or breaks down such a byte stream. 35 | 36 | Normally you don't need to deal with anything this low-level, but it's 37 | exposed for people implementing custom extensions, or features that 38 | paramiko doesn't support yet. 39 | """ 40 | 41 | big_int = long(0xff000000) 42 | 43 | def __init__(self, content=None): 44 | """ 45 | Create a new SSH2 message. 46 | 47 | :param str content: 48 | the byte stream to use as the message content (passed in only when 49 | decomposing a message). 50 | """ 51 | if content is not None: 52 | self.packet = BytesIO(content) 53 | else: 54 | self.packet = BytesIO() 55 | 56 | def __str__(self): 57 | """ 58 | Return the byte stream content of this message, as a string/bytes obj. 59 | """ 60 | return self.asbytes() 61 | 62 | def __repr__(self): 63 | """ 64 | Returns a string representation of this object, for debugging. 65 | """ 66 | return 'paramiko.Message(' + repr(self.packet.getvalue()) + ')' 67 | 68 | def asbytes(self): 69 | """ 70 | Return the byte stream content of this Message, as bytes. 71 | """ 72 | return self.packet.getvalue() 73 | 74 | def rewind(self): 75 | """ 76 | Rewind the message to the beginning as if no items had been parsed 77 | out of it yet. 78 | """ 79 | self.packet.seek(0) 80 | 81 | def get_remainder(self): 82 | """ 83 | Return the bytes (as a `str`) of this message that haven't already been 84 | parsed and returned. 85 | """ 86 | position = self.packet.tell() 87 | remainder = self.packet.read() 88 | self.packet.seek(position) 89 | return remainder 90 | 91 | def get_so_far(self): 92 | """ 93 | Returns the `str` bytes of this message that have been parsed and 94 | returned. The string passed into a message's constructor can be 95 | regenerated by concatenating ``get_so_far`` and `get_remainder`. 96 | """ 97 | position = self.packet.tell() 98 | self.rewind() 99 | return self.packet.read(position) 100 | 101 | def get_bytes(self, n): 102 | """ 103 | Return the next ``n`` bytes of the message (as a `str`), without 104 | decomposing into an int, decoded string, etc. Just the raw bytes are 105 | returned. Returns a string of ``n`` zero bytes if there weren't ``n`` 106 | bytes remaining in the message. 107 | """ 108 | b = self.packet.read(n) 109 | max_pad_size = 1 << 20 # Limit padding to 1 MB 110 | if len(b) < n < max_pad_size: 111 | return b + zero_byte * (n - len(b)) 112 | return b 113 | 114 | def get_byte(self): 115 | """ 116 | Return the next byte of the message, without decomposing it. This 117 | is equivalent to `get_bytes(1) `. 118 | 119 | :return: 120 | the next (`str`) byte of the message, or ``'\000'`` if there aren't 121 | any bytes remaining. 122 | """ 123 | return self.get_bytes(1) 124 | 125 | def get_boolean(self): 126 | """ 127 | Fetch a boolean from the stream. 128 | """ 129 | b = self.get_bytes(1) 130 | return b != zero_byte 131 | 132 | def get_adaptive_int(self): 133 | """ 134 | Fetch an int from the stream. 135 | 136 | :return: a 32-bit unsigned `int`. 137 | """ 138 | byte = self.get_bytes(1) 139 | if byte == max_byte: 140 | return util.inflate_long(self.get_binary()) 141 | byte += self.get_bytes(3) 142 | return struct.unpack('>I', byte)[0] 143 | 144 | def get_int(self): 145 | """ 146 | Fetch an int from the stream. 147 | """ 148 | return struct.unpack('>I', self.get_bytes(4))[0] 149 | 150 | def get_int64(self): 151 | """ 152 | Fetch a 64-bit int from the stream. 153 | 154 | :return: a 64-bit unsigned integer (`long`). 155 | """ 156 | return struct.unpack('>Q', self.get_bytes(8))[0] 157 | 158 | def get_mpint(self): 159 | """ 160 | Fetch a long int (mpint) from the stream. 161 | 162 | :return: an arbitrary-length integer (`long`). 163 | """ 164 | return util.inflate_long(self.get_binary()) 165 | 166 | def get_string(self): 167 | """ 168 | Fetch a `str` from the stream. This could be a byte string and may 169 | contain unprintable characters. (It's not unheard of for a string to 170 | contain another byte-stream message.) 171 | """ 172 | return self.get_bytes(self.get_int()) 173 | 174 | def get_text(self): 175 | """ 176 | Fetch a Unicode string from the stream. 177 | """ 178 | return u(self.get_string()) 179 | 180 | def get_binary(self): 181 | """ 182 | Fetch a string from the stream. This could be a byte string and may 183 | contain unprintable characters. (It's not unheard of for a string to 184 | contain another byte-stream Message.) 185 | """ 186 | return self.get_bytes(self.get_int()) 187 | 188 | def get_list(self): 189 | """ 190 | Fetch a `list` of `strings ` from the stream. 191 | 192 | These are trivially encoded as comma-separated values in a string. 193 | """ 194 | return self.get_text().split(',') 195 | 196 | def add_bytes(self, b): 197 | """ 198 | Write bytes to the stream, without any formatting. 199 | 200 | :param str b: bytes to add 201 | """ 202 | self.packet.write(b) 203 | return self 204 | 205 | def add_byte(self, b): 206 | """ 207 | Write a single byte to the stream, without any formatting. 208 | 209 | :param str b: byte to add 210 | """ 211 | self.packet.write(b) 212 | return self 213 | 214 | def add_boolean(self, b): 215 | """ 216 | Add a boolean value to the stream. 217 | 218 | :param bool b: boolean value to add 219 | """ 220 | if b: 221 | self.packet.write(one_byte) 222 | else: 223 | self.packet.write(zero_byte) 224 | return self 225 | 226 | def add_int(self, n): 227 | """ 228 | Add an integer to the stream. 229 | 230 | :param int n: integer to add 231 | """ 232 | self.packet.write(struct.pack('>I', n)) 233 | return self 234 | 235 | def add_adaptive_int(self, n): 236 | """ 237 | Add an integer to the stream. 238 | 239 | :param int n: integer to add 240 | """ 241 | if n >= Message.big_int: 242 | self.packet.write(max_byte) 243 | self.add_string(util.deflate_long(n)) 244 | else: 245 | self.packet.write(struct.pack('>I', n)) 246 | return self 247 | 248 | def add_int64(self, n): 249 | """ 250 | Add a 64-bit int to the stream. 251 | 252 | :param long n: long int to add 253 | """ 254 | self.packet.write(struct.pack('>Q', n)) 255 | return self 256 | 257 | def add_mpint(self, z): 258 | """ 259 | Add a long int to the stream, encoded as an infinite-precision 260 | integer. This method only works on positive numbers. 261 | 262 | :param long z: long int to add 263 | """ 264 | self.add_string(util.deflate_long(z)) 265 | return self 266 | 267 | def add_string(self, s): 268 | """ 269 | Add a string to the stream. 270 | 271 | :param str s: string to add 272 | """ 273 | s = asbytes(s) 274 | self.add_int(len(s)) 275 | self.packet.write(s) 276 | return self 277 | 278 | def add_list(self, l): 279 | """ 280 | Add a list of strings to the stream. They are encoded identically to 281 | a single string of values separated by commas. (Yes, really, that's 282 | how SSH2 does it.) 283 | 284 | :param list l: list of strings to add 285 | """ 286 | self.add_string(','.join(l)) 287 | return self 288 | 289 | def _add(self, i): 290 | if type(i) is bool: 291 | return self.add_boolean(i) 292 | elif isinstance(i, integer_types): 293 | return self.add_adaptive_int(i) 294 | elif type(i) is list: 295 | return self.add_list(i) 296 | else: 297 | return self.add_string(i) 298 | 299 | def add(self, *seq): 300 | """ 301 | Add a sequence of items to the stream. The values are encoded based 302 | on their type: str, int, bool, list, or long. 303 | 304 | .. warning:: 305 | Longs are encoded non-deterministically. Don't use this method. 306 | 307 | :param seq: the sequence of items 308 | """ 309 | for item in seq: 310 | self._add(item) 311 | -------------------------------------------------------------------------------- /paramiko/ecdsakey.py: -------------------------------------------------------------------------------- 1 | # Copyright (C) 2003-2007 Robey Pointer 2 | # 3 | # This file is part of paramiko. 4 | # 5 | # Paramiko is free software; you can redistribute it and/or modify it under the 6 | # terms of the GNU Lesser General Public License as published by the Free 7 | # Software Foundation; either version 2.1 of the License, or (at your option) 8 | # any later version. 9 | # 10 | # Paramiko is distrubuted in the hope that it will be useful, but WITHOUT ANY 11 | # WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR 12 | # A PARTICULAR PURPOSE. See the GNU Lesser General Public License for more 13 | # details. 14 | # 15 | # You should have received a copy of the GNU Lesser General Public License 16 | # along with Paramiko; if not, write to the Free Software Foundation, Inc., 17 | # 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA. 18 | 19 | """ 20 | ECDSA keys 21 | """ 22 | 23 | from cryptography.exceptions import InvalidSignature 24 | from cryptography.hazmat.backends import default_backend 25 | from cryptography.hazmat.primitives import hashes, serialization 26 | from cryptography.hazmat.primitives.asymmetric import ec 27 | from cryptography.hazmat.primitives.asymmetric.utils import ( 28 | decode_dss_signature, encode_dss_signature 29 | ) 30 | 31 | from paramiko.common import four_byte 32 | from paramiko.message import Message 33 | from paramiko.pkey import PKey 34 | from paramiko.ssh_exception import SSHException 35 | from paramiko.util import deflate_long 36 | 37 | 38 | class _ECDSACurve(object): 39 | """ 40 | Represents a specific ECDSA Curve (nistp256, nistp384, etc). 41 | 42 | Handles the generation of the key format identifier and the selection of 43 | the proper hash function. Also grabs the proper curve from the 'ecdsa' 44 | package. 45 | """ 46 | def __init__(self, curve_class, nist_name): 47 | self.nist_name = nist_name 48 | self.key_length = curve_class.key_size 49 | 50 | # Defined in RFC 5656 6.2 51 | self.key_format_identifier = "ecdsa-sha2-" + self.nist_name 52 | 53 | # Defined in RFC 5656 6.2.1 54 | if self.key_length <= 256: 55 | self.hash_object = hashes.SHA256 56 | elif self.key_length <= 384: 57 | self.hash_object = hashes.SHA384 58 | else: 59 | self.hash_object = hashes.SHA512 60 | 61 | self.curve_class = curve_class 62 | 63 | 64 | class _ECDSACurveSet(object): 65 | """ 66 | A collection to hold the ECDSA curves. Allows querying by oid and by key 67 | format identifier. The two ways in which ECDSAKey needs to be able to look 68 | up curves. 69 | """ 70 | def __init__(self, ecdsa_curves): 71 | self.ecdsa_curves = ecdsa_curves 72 | 73 | def get_key_format_identifier_list(self): 74 | return [curve.key_format_identifier for curve in self.ecdsa_curves] 75 | 76 | def get_by_curve_class(self, curve_class): 77 | for curve in self.ecdsa_curves: 78 | if curve.curve_class == curve_class: 79 | return curve 80 | 81 | def get_by_key_format_identifier(self, key_format_identifier): 82 | for curve in self.ecdsa_curves: 83 | if curve.key_format_identifier == key_format_identifier: 84 | return curve 85 | 86 | def get_by_key_length(self, key_length): 87 | for curve in self.ecdsa_curves: 88 | if curve.key_length == key_length: 89 | return curve 90 | 91 | 92 | class ECDSAKey(PKey): 93 | """ 94 | Representation of an ECDSA key which can be used to sign and verify SSH2 95 | data. 96 | """ 97 | 98 | _ECDSA_CURVES = _ECDSACurveSet([ 99 | _ECDSACurve(ec.SECP256R1, 'nistp256'), 100 | _ECDSACurve(ec.SECP384R1, 'nistp384'), 101 | _ECDSACurve(ec.SECP521R1, 'nistp521'), 102 | ]) 103 | 104 | def __init__(self, msg=None, data=None, filename=None, password=None, 105 | vals=None, file_obj=None, validate_point=True): 106 | self.verifying_key = None 107 | self.signing_key = None 108 | if file_obj is not None: 109 | self._from_private_key(file_obj, password) 110 | return 111 | if filename is not None: 112 | self._from_private_key_file(filename, password) 113 | return 114 | if (msg is None) and (data is not None): 115 | msg = Message(data) 116 | if vals is not None: 117 | self.signing_key, self.verifying_key = vals 118 | c_class = self.signing_key.curve.__class__ 119 | self.ecdsa_curve = self._ECDSA_CURVES.get_by_curve_class(c_class) 120 | else: 121 | if msg is None: 122 | raise SSHException('Key object may not be empty') 123 | self.ecdsa_curve = self._ECDSA_CURVES.get_by_key_format_identifier( 124 | msg.get_text()) 125 | if self.ecdsa_curve is None: 126 | raise SSHException('Invalid key') 127 | curvename = msg.get_text() 128 | if curvename != self.ecdsa_curve.nist_name: 129 | raise SSHException("Can't handle curve of type %s" % curvename) 130 | 131 | pointinfo = msg.get_binary() 132 | try: 133 | numbers = ec.EllipticCurvePublicNumbers.from_encoded_point( 134 | self.ecdsa_curve.curve_class(), pointinfo 135 | ) 136 | except ValueError: 137 | raise SSHException("Invalid public key") 138 | self.verifying_key = numbers.public_key(backend=default_backend()) 139 | 140 | @classmethod 141 | def supported_key_format_identifiers(cls): 142 | return cls._ECDSA_CURVES.get_key_format_identifier_list() 143 | 144 | def asbytes(self): 145 | key = self.verifying_key 146 | m = Message() 147 | m.add_string(self.ecdsa_curve.key_format_identifier) 148 | m.add_string(self.ecdsa_curve.nist_name) 149 | 150 | numbers = key.public_numbers() 151 | 152 | key_size_bytes = (key.curve.key_size + 7) // 8 153 | 154 | x_bytes = deflate_long(numbers.x, add_sign_padding=False) 155 | x_bytes = b'\x00' * (key_size_bytes - len(x_bytes)) + x_bytes 156 | 157 | y_bytes = deflate_long(numbers.y, add_sign_padding=False) 158 | y_bytes = b'\x00' * (key_size_bytes - len(y_bytes)) + y_bytes 159 | 160 | point_str = four_byte + x_bytes + y_bytes 161 | m.add_string(point_str) 162 | return m.asbytes() 163 | 164 | def __str__(self): 165 | return self.asbytes() 166 | 167 | def __hash__(self): 168 | return hash((self.get_name(), self.verifying_key.public_numbers().x, 169 | self.verifying_key.public_numbers().y)) 170 | 171 | def get_name(self): 172 | return self.ecdsa_curve.key_format_identifier 173 | 174 | def get_bits(self): 175 | return self.ecdsa_curve.key_length 176 | 177 | def can_sign(self): 178 | return self.signing_key is not None 179 | 180 | def sign_ssh_data(self, data): 181 | ecdsa = ec.ECDSA(self.ecdsa_curve.hash_object()) 182 | signer = self.signing_key.signer(ecdsa) 183 | signer.update(data) 184 | sig = signer.finalize() 185 | r, s = decode_dss_signature(sig) 186 | 187 | m = Message() 188 | m.add_string(self.ecdsa_curve.key_format_identifier) 189 | m.add_string(self._sigencode(r, s)) 190 | return m 191 | 192 | def verify_ssh_sig(self, data, msg): 193 | if msg.get_text() != self.ecdsa_curve.key_format_identifier: 194 | return False 195 | sig = msg.get_binary() 196 | sigR, sigS = self._sigdecode(sig) 197 | signature = encode_dss_signature(sigR, sigS) 198 | 199 | verifier = self.verifying_key.verifier( 200 | signature, ec.ECDSA(self.ecdsa_curve.hash_object()) 201 | ) 202 | verifier.update(data) 203 | try: 204 | verifier.verify() 205 | except InvalidSignature: 206 | return False 207 | else: 208 | return True 209 | 210 | def write_private_key_file(self, filename, password=None): 211 | self._write_private_key_file( 212 | filename, 213 | self.signing_key, 214 | serialization.PrivateFormat.TraditionalOpenSSL, 215 | password=password 216 | ) 217 | 218 | def write_private_key(self, file_obj, password=None): 219 | self._write_private_key( 220 | file_obj, 221 | self.signing_key, 222 | serialization.PrivateFormat.TraditionalOpenSSL, 223 | password=password 224 | ) 225 | 226 | @classmethod 227 | def generate(cls, curve=ec.SECP256R1(), progress_func=None, bits=None): 228 | """ 229 | Generate a new private ECDSA key. This factory function can be used to 230 | generate a new host key or authentication key. 231 | 232 | :param progress_func: Not used for this type of key. 233 | :returns: A new private key (`.ECDSAKey`) object 234 | """ 235 | if bits is not None: 236 | curve = cls._ECDSA_CURVES.get_by_key_length(bits) 237 | if curve is None: 238 | raise ValueError("Unsupported key length: %d" % bits) 239 | curve = curve.curve_class() 240 | 241 | private_key = ec.generate_private_key(curve, backend=default_backend()) 242 | return ECDSAKey(vals=(private_key, private_key.public_key())) 243 | 244 | # ...internals... 245 | 246 | def _from_private_key_file(self, filename, password): 247 | data = self._read_private_key_file('EC', filename, password) 248 | self._decode_key(data) 249 | 250 | def _from_private_key(self, file_obj, password): 251 | data = self._read_private_key('EC', file_obj, password) 252 | self._decode_key(data) 253 | 254 | def _decode_key(self, data): 255 | try: 256 | key = serialization.load_der_private_key( 257 | data, password=None, backend=default_backend() 258 | ) 259 | except (ValueError, AssertionError) as e: 260 | raise SSHException(str(e)) 261 | 262 | self.signing_key = key 263 | self.verifying_key = key.public_key() 264 | curve_class = key.curve.__class__ 265 | self.ecdsa_curve = self._ECDSA_CURVES.get_by_curve_class(curve_class) 266 | 267 | def _sigencode(self, r, s): 268 | msg = Message() 269 | msg.add_mpint(r) 270 | msg.add_mpint(s) 271 | return msg.asbytes() 272 | 273 | def _sigdecode(self, sig): 274 | msg = Message(sig) 275 | r = msg.get_mpint() 276 | s = msg.get_mpint() 277 | return r, s 278 | --------------------------------------------------------------------------------