《Python 黑帽子》学习笔记 - Python3 实现 netcat - Day 8

原书的代码主要考虑的是如何实现功能,在字符编码,socket 阻塞和数据交互,异常处理等方面存在一些问题,造成了程序功能不完善,逻辑出差和退出等情况。

本篇笔记记录用 Python3 实现原书的 netcat, 脚本功能和步骤主要是参照原书的实现思路,会对部分代码的逻辑进行更合理的调整,并学习字符编码,异常处理,调试日志记录等知识点。

功能和实现思路见上一篇笔记。

Python3 代码

#!/usr/bin/env python3
# -*- code: utf-8 -*-

import sys
import getopt
import socket
import subprocess
import threading
import logging

logging.basicConfig(level=logging.DEBUG,
                    format='%(filename)s[line:%(lineno)d] %(levelname)s: %(message)s',
                    # format='%(asctime)s %(filename)s[line:%(lineno)d] %(levelname)s: %(message)s',
                    # datefmt='%Y/%m/%d %H:%M:%S',
                    # filename='myapp.log',
                    filemode='a')

# define some global variables
listen = False
command = False
upload = False
execute = ""
target = ""
upload_destination = ""
port = 0

def run_command(command):
    """
    execute the shell command, or file received from client.
    :param command: 
    :return: output: shell command result. 
    """
    # trim the newline.(delete the characters of the string end.)
    command = command.rstrip()

    # run the command and get the output back
    try:
        # run command with arguments and return its output.
        output = subprocess.check_output(command, stderr=subprocess.STDOUT, shell=True)
        logging.debug(output)
    except Exception as e:
        logging.error(e)
        output = b"Failed to execute command.\r\n"

    # send the output back to the client
    return output

def client_handler(client_socket):
    """
    the thread function of handling the client. 
    :param client_socket: 
    :return: 
    """
    global upload
    global execute
    global command

    # upload file
    if len(upload_destination):
        # read in all of the bytes and write to our destination
        file_buffer = ""
        # keep reading data until none is available
        while True:
            data = client_socket.recv(1024)
            file_buffer += data.decode("utf-8")
            logging.debug(data)

            # "#EOF#" tell the server, file is end.
            if "#EOF#" in file_buffer:
                file_buffer = file_buffer[:-6]
                break
            # for interaciton, like heart packet.
            client_socket.send(b"#")

        # now we take these bytes and try to write them out
        try:
            with open(upload_destination, "wb") as fw:
                fw.write(file_buffer.encode("utf-8"))
            client_socket.send(b"save file successed.\n")
        except Exception as err:
            logging.error(err)
            client_socket.send(b"save file failed.\n")
        finally:
            client_socket.close()

    # execute the given file
    if len(execute):
        # run the command
        output = run_command(execute)

        client_socket.send(output)

    # now we go into another loop if a command shell was requested
    if command:
        # receive command from client, execute it, and send the result data.
        try:
            while True:
                # show a simple prompt
                client_socket.send(b"<BHP:#>")
                # now we receive until we see a linefeed (enter key)
                cmd_buffer = ""
                while "\n" not in cmd_buffer:
                    try:
                        cmd_buffer += client_socket.recv(1024).decode("utf-8")
                    except Exception as err:
                        logging.error(err)
                        client_socket.close()
                        break
                # we have a valid command so execute it and send back the results
                response = run_command(cmd_buffer)
                # send back the response
                client_socket.send(response)
        except Exception as err:
            logging.error(err)
            client_socket.close()


def server_loop():
    """
    the server listen. create a thread to handle client's connection.
    :return: 
    """
    global target
    global port

    # if no target is defined we listen on all interfaces
    if not len(target):
        target = "0.0.0.0"

    server = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
    server.bind((target, port))
    logging.info("Listen    %s:%d" % (target, port))
    server.listen(5)

    while True:
        client_socket, addr = server.accept()

        # spin off a thread to handle our new client
        client_thread = threading.Thread(target=client_handler, args=(client_socket,))
        client_thread.start()


def client_sender(buffer):
    """
    the client send datas to the server, and receive datas from server.
    :param buffer: datas from the stdin
    :return: 
    """
    client = socket.socket(socket.AF_INET, socket.SOCK_STREAM)

    try:
        # conncet to target
        client.connect((target, port))
        logging.debug('server is %s:%d' % (target, port))

        # if we detect input from stdin then send the datas.
        # if not we are going to wait for the user to input.
        if len(buffer):
            # send the datas with utf-8 endecode.
            client.send(buffer.encode("utf-8"))

        while True:
            # now wait for datas back
            recv_len = 1
            response = ""

            while recv_len:
                data = client.recv(4096)
                logging.debug("receive datas : %s" % data)

                try:
                    response += data.decode("utf-8")
                except Exception as e:
                    logging.error(e)
                    response += data.decode("gbk")

                if recv_len < 4096:
                    break

            print(response + " ")

            # wait for more input
            # Python2 is raw_input(), and Python3 is input()
            buffer = input("")
            buffer += "\n"

            client.send(buffer.encode("utf-8"))
            # logging.info("send datas: %s" % buffer)

    except Exception as e:
        logging.error(e)

    finally:
        # teardown the connection
        client.close()


def usage():
    """
    print the info of help
    :return: 
    """
    print("Usage: netcat.py -t target_host -p port")
    print("\t-l --listen                - listen on [host]:[port] for incoming connections")
    print("\t-e --execute=file_to_run   - execute the given file upon receiving a connection")
    print("\t-c --command               - initialize a command shell")
    print("\t-u --upload=destination    - upon receiving connection upload a file and write to [destination]")
    print("Examples: ")
    print("\tnetcat.py -t 192.168.1.3 -p 5555 -l -c")
    print("\tnetcat.py -t 192.168.1.3 -p 5555 -l -u=c:\\target.exe")
    print("\tnetcat.py -t 192.168.1.3 -p 5555 -l -e=\"cat /etc/passwd\"")
    print("\techo 'ABCDEFGHI' | ./netcat.py.py -t192.168.1.7 -p80")
    sys.exit(0)


def main():
    """
    parse shell option and parameters, and set the vars.
    call listen function or connect function.
    :return: 
    """
    global listen
    global port
    global execute
    global command
    global upload_destination
    global target

    if not len(sys.argv[1:]):
        usage()

    # read the commandline options
    try:
        opts, args = getopt.getopt(sys.argv[1:], "hle:t:p:cu:",
                                   ["help", "listen", "execute=", "target=", "port=", "command", "upload="])
    except getopt.GetoptError as err:
        logging.error("%s", 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"
            usage()

    # are we going to listen or just send data from stdin
    if not listen and len(target) and port > 0:
        # read in the buffer from the commandline
        # this will block, so send CTRL-D if not sending input to stdin
        # Windows is Ctrl-Z
        # buffer = sys.stdin.read()
        buffer = input() + '\n'

        # send data off
        client_sender(buffer)

    # we are going to listen and potentially
    # upload things, execute commands and drop a shell back
    # depending on our command line options above
    if listen:
        server_loop()

main()

测试

命令行 shell 功能:

shell-py3-debug

带 debug 的很乱。

shell-py3-error

logging 的 level 设置为 ERROR,不会输出 debug 信息。

upload 功能

我修改为当客户端发送 #EOF# 后,作为文件传输的结束,并在服务端发送一个反馈数据 #, 以保障双方能数据交互,不然 socket.recv() 将一直阻塞,也可以考虑修改 socket 的超时设置。

upload-py3

命令执行功能

exec-py3

异常和调试

在编写代码的时候,用 logging 调试数据通信和异常错误,帮我解决了很多问题。简单记录下,更详细的知识使用到时再去查阅。

异常处理代码结构如下,把可能会引发异常的代码放在 try 后执行,引发异常会执行 except 里的代码,最后会执行 finall 里的代码,可以把关闭套接字,退出程序等善后的代码放在这里。

try:
    ...
except Exception as e:
    logging.error(e)
finally:
    ...

用 Python 自带的 logging 模块,可以直观的在终端看到调试信息,或把调试信息存到文件里,比 print() 函数要方便很多,能够显示出调试信息出自程序的哪一行代码,可以通过设置不同的日志等级(level)来输出不同日志信息,设置高等级的日志等级后,低等级的日志信息不会输出。

level 值的说明:

  • FATAL 致命错误
  • CRITICAL 特别糟糕的事情,如内存耗尽、磁盘空间为空,一般很少使用
  • ERROR 发生错误时,如 IO 操作失败或者连接问题
  • WARNING 发生很重要的事件,但是并不是错误时,如用户登录密码错误
  • INFO 处理请求或者状态变化等日常事务
  • DEBUG 调试过程中使用 DEBUG 等级,如算法中每个循环的中间状态

format 说明:

  • %(levelno)s:打印日志级别的数值
  • %(levelname)s:打印日志级别的名称
  • %(pathname)s:打印当前执行程序的路径,其实就是sys.argv[0]
  • %(filename)s:打印当前执行程序名
  • %(funcName)s:打印日志的当前函数
  • %(lineno)d:打印日志的当前行号
  • %(asctime)s:打印日志的时间
  • %(thread)d:打印线程ID
  • %(threadName)s:打印线程名称
  • %(process)d:打印进程ID
  • %(message)s:打印日志信息
import logging

logging.basicConfig(level=logging.DEBUG,
                    format='%(asctime)s %(filename)s[line:%(lineno)d] %(levelname)s: %(message)s',
                    datefmt='%Y/%m/%d %H:%M:%S',
                    # filename='myapp.log',
                    filemode='a')

参考:https://www.cnblogs.com/liujiacai/p/7804848.html

编码问题

实现命令行 shell 功能时,在 Win7 中文系统上测试,需要传输中文字符,出现 UnicodeDecodeError 错误。即:

netcat-p3.py[line:168] DEBUG: receive datas : b'\r\nMicrosoft Windows [\xb0\xe6\xb1\xbe 6.1.7601]\r\n<BHP:#>'
netcat-p3.py[line:173] ERROR: 'utf-8' codec can't decode byte 0xb0 in position 21: invalid start byte

对应代码是:

try:
    response += data.decode("utf-8")  # 异常的地方
except Exception as e:
    logging.error(e)
    response += data.decode("gbk")

来分析下这个异常的原因。异常出在要把

b'\r\nMicrosoft Windows [\xb0\xe6\xb1\xbe 6.1.7601]\r\n<BHP:#>'

这段数据进行 decode(“utf-8”) 解码。这段数据的来源是创建 shell 子进程后,运行 ver 命令后的结果,即中文的

Microsoft Windows [版本 6.1.7601]

shell 子进程输出结果数据的编码是跟随运行 shell 的系统的,或者说是跟随当前启动 shell 的终端的数据编码。而当前终端数据的编码是 cp936, 近似于 gbk 编码。实际上中文 Win7 系统内部都是 cp936.

>>> import locale
>>> locale.getdefaultlocale()  # 获取系统当前的编码
('zh_CN', 'cp936')

可以理解为,这段数据的编码是 gbk 编码,而 utf-8 和 gbk 编码之间是不能直接转换的,所有的 utf-8 和 gbk 编码都得通过 unicode 编码进行转换。

参考:https://blog.csdn.net/chixujohnny/article/details/51782826

所以,在将 shell 子进程的结果数据,直接进行 decode(“utf-8”) 解码,会引发 UnicodeDecodeError 异常。我在修改代码时,添加了一个异常处理,如果 utf-8 解码失败,会修改为 gbk 解码。这样能保证程序不会因为异常而退出。

再说明下,为什么要先进行 utf-8 解码?因为要保证 socket 通信使用 byte 流传输,我对大多数要通信的数据(基本都是 str)用 utf-8 进行了编码,编码后即为 byte 流,发送前 encode, 接收后 decode. 由于创建 shell 子进程后,其输出结果直接就是 byte 流,所以没对其进行编码转换,直接通过 socket.send() 发送。

执行 shell 的代码如下,用 logging.debug(output), 可以看到输出数据为 byte 流。

# run the command and get the output back
try:
    # run command with arguments and return its output.
    output = subprocess.check_output(command, stderr=subprocess.STDOUT, shell=True)
    logging.debug(output)
except Exception as e:
    logging.error(e)
    output = b"Failed to execute command.\r\n"

netcat-p3.py[line:40] DEBUG: b'\r\nMicrosoft Windows [\xb0\xe6\xb1\xbe 6.1.7601]\r\n'

查看系统编码

用 Python 自带的 locale 模块可以检测命令行的默认编码(也是系统的编码),和设置命令行编码。

我的 Kali 英文系统,编码为 utf-8.

>>> import locale
>>> locale.getdefaultlocale()
('en_US', 'UTF-8')

我的 Win7 中文系统,编码为 cp936.

>>> import locale
>>> locale.getdefaultlocale()
('zh_CN', 'cp936')

总结

关于 netcat 的实现,主要是解决了一些异常和逻辑的问题,还可以有很多完善的地方,考虑加快下学习进度,下步的笔记将主要记录代码的实现。

要想把一个知识点用文字清楚地表达出来,哪怕是简单的知识点,也要花费很多精力。

  • 1
    点赞
  • 5
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值