原书的代码主要考虑的是如何实现功能,在字符编码,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 功能:
带 debug 的很乱。
logging 的 level 设置为 ERROR,不会输出 debug 信息。
upload 功能
我修改为当客户端发送 #EOF#
后,作为文件传输的结束,并在服务端发送一个反馈数据 #
, 以保障双方能数据交互,不然 socket.recv() 将一直阻塞,也可以考虑修改 socket 的超时设置。
命令执行功能
异常和调试
在编写代码的时候,用 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 的实现,主要是解决了一些异常和逻辑的问题,还可以有很多完善的地方,考虑加快下学习进度,下步的笔记将主要记录代码的实现。
要想把一个知识点用文字清楚地表达出来,哪怕是简单的知识点,也要花费很多精力。