1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
import argparse
import socket
import shlex
# subprocess这个库提供了一组强大的进程创建接口,可以通过多种方式调用其他程序。
import subprocess
import sys
import textwrap
import threading

# 这个函数将会接受一条命令并执行,然后将结果作为一段字符串返回。
def execute(cmd):
cmd = cmd.strip()
if not cmd:
return
# subprocess用到了它的check_output函数,这个函数会在本机运行一条命令,并返回该命令的输出。
output = subprocess.check_output(shlex.split(cmd), stderr=subprocess.STDOUT)
return output.decode()

class NetCat:
# main代码块传进来的命令行参数和缓冲区数据,初始化一个NetCat对象
def __init__(self, args, buffer=None):
self.args = args
self.buffer = buffer
# 然后创建一个socket对象
self.socket = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
self.socket.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)


def run(self):
# 如果我们的NetCat对象是接收方,run就执行listen函数
if self.args.listen:
self.listen()
# 如果是发送方,run就执行send函数
else:
self.send()

def send(self):
# 先连接到target和port
self.socket.connect((self.args.target, self.args.port))
# 如果这时缓冲区里有数据的话,就先把这些数据发过去。
if self.buffer:
self.socket.send(self.buffer)

# 创建一个try/catch块,这样就能直接用Ctrl+C组合键手动关闭连接
try:
# 创建一个大循环,一轮一轮地接收target返回的数据
while True:
recv_len = 1
response = ''
# 在大循环里,建了一个小循环,用来读取socket本轮返回的数据
while recv_len:
data = self.socket.recv(4096)
recv_len = len(data)
response += data.decode()
# 如果socket里的数据目前已经读到头,就退出小循环
if recv_len < 4096:
break
if response:
print(response)
buffer = input('> ')
buffer += '\n'
# 检查刚才有没有实际读出什么东西来,如果读出了什么,就输出到屏幕上,
# 并暂停,等待用户输入新的内容,再把新的内容发给target
self.socket.send(buffer.encode())
# 接着开始下一轮大循环
# Ctrl+C组合键触发KeyboardInterrupt中断循环
except KeyboardInterrupt:
print('User terminated.')
self.socket.close()
sys.exit()

def listen(self):
# listen函数把socket对象绑定到target和port上
self.socket.bind((self.args.target, self.args.port))
self.socket.listen(5)
# 开始用一个循环监听新连接
while True:
client_socket, _ = self.socket.accept()
# 并把已连接的socket对象传递给handle函数
client_thread = threading.Thread(
target=self.handle, args=(client_socket,)
)
client_thread.start()

def handle(self, client_socket):
# 如果要执行命令,handle函数就会把该命令传递给execute函数,然后把输出结果通过socket发回去。
if self.args.execute:
output = execute(self.args.execute)
client_socket.send(output.encode())

# 如果要上传文件,我们就建一个循环来接收socket传来的文件内容
# 再将收到的全部数据写到参数指定的文件里。
elif self.args.upload:
file_buffer = b''
while True:
data = client_socket.recv(4096)
if data:
file_buffer += data
else:
break

with open(self.args.upload, 'wb') as f:
f.write(file_buffer)
message = f'Saved file {self.args.upload}'
client_socket.send(message.encode())

# 如果要创建一个shell,创建一个循环,向发送方发一个提示符,然后等待其发回命令。
# 每收到一条命令,就用execute函数执行它,然后把结果发回发送方。
elif self.args.command:
cmd_buffer = b''
while True:
try:
client_socket.send(b'BHP: #> ')
# shell是收到换行符后才执行命令的
while '\n' not in cmd_buffer.decode():
cmd_buffer += client_socket.recv(64)
response = execute(cmd_buffer.decode())
if response:
client_socket.send(response.encode())
cmd_buffer = b''

except Exception as e:
print(f'server killed {e}')
self.socket.close()
sys.exit()


if __name__ == '__main__':
# 标准库里的argparse库创建了一个带命令行界面的程序
# 传递不同的参数,就能控制这个程序执行不同的操作
# 比如上传文件、远程执行命令,或是打开一个命令行shell。
parser = argparse.ArgumentParser(
description='BHP Net Tool',
formatter_class=argparse.RawDescriptionHelpFormatter,
# 编写了一段帮助信息,程序启动的时候如果发现--help参数,就会显示这段信息。
epilog=textwrap.dedent('''Example:
netcat.py -t 192.168.152.128 -p 6666 -l -c # command shell
netcat.py -t 192.168.152.128 -p 6666 -l -u=mytest.txt # upload to file
netcat.py -t 192.168.152.128 -p 6666 -l -e=\"cat /etc/passwd\" # execute command
echo 'ABC' | ./netcat.py -t 192.168.152.128 -p 135 # echo text to server port 135
netcat.py -t 192.168.152.128 -p 6666 # connect to server
'''))
# 添加了6个参数,用来控制程序的行为
# 使用了-c、-e和-u这三个参数,就意味着要使用-l参数,因为这些行为都只能由接收方来完成。
# 程序收到-c参数,就会打开一个交互式的命令行shell;
parser.add_argument('-c', '--command', action='store_true', help='command shell')
# 收到-e参数,就会执行一条命令;
parser.add_argument('-e', '--execute', help='execute specified command')
# 收到-l参数,就会创建一个监听器;
parser.add_argument('-l', '--listen', action='store_true', help='listen')
# -p参数用来指定要通信的端口;
parser.add_argument('-p', "--port", type=int, default=6666, help='specified port')
# -t参数用来指定要通信的目标IP地址;
parser.add_argument('-t', '--target', default='192.168.152.128', help='specified IP')
# -u参数用来指定要上传的文件。
parser.add_argument('-u', '--upload', help='upload file')
args = parser.parse_args()
# 如果确定了程序要进行监听,我们就在缓冲区里填上空白数据,把空白缓冲区传给NetCat对象。
# 反之,我们就把stdin里的数据通过缓冲区传进去。最后调用NetCat类的run函数来启动它。
if args.listen:
buffer = ''
else:
buffer = sys.stdin.read()

nc = NetCat(args, buffer.encode())
nc.run()

内置类型 str.strip([chars])

str.strip([chars])

返回原字符串的副本,移除其中的前导和末尾字符。 chars 参数为指定要移除字符的字符串。 如果省略或为 None,则 chars 参数默认移除空白符。 实际上 chars 参数并非指定单个前缀或后缀;而是会移除参数值的所有组合:

1
2
3
4
>>> '   spacious   '.strip()
'spacious'
>>> 'www.example.com'.strip('cmowz.')
'example'

最外侧的前导和末尾 chars 参数值将从字符串中移除。 开头端的字符的移除将在遇到一个未包含于 chars 所指定字符集的字符时停止。 类似的操作也将在结尾端发生。 例如:

1
2
3
>>> comment_string = '#....... Section 3.2.1 Issue #32 .......'
>>> comment_string.strip('.#! ')
'Section 3.2.1 Issue #32'

subprocess — 子进程管理

subprocess 模块允许你生成新的进程,连接它们的输入、输出、错误管道,并且获取它们的返回码。此模块打算代替一些老旧的模块与功能:

check_output

subprocess.check_output(args, *, stdin=None, stderr=None, shell=False, cwd=None, encoding=None, errors=None, universal_newlines=None, timeout=None*, *text=None, **other_popen_kwargs)

附带参数运行命令并返回其输出。

如果返回码非零则会引发 CalledProcessErrorCalledProcessError 对象将在 returncode 属性中保存返回码并在 output 属性中保存所有输出。

这相当于:

1
run(..., check=True, stdout=PIPE).stdout

上面显示的参数只是常见的一些。 完整的函数签名与 run() 的大致相同 —— 大部分参数会通过该接口直接传递。 存在一个与 run() 行为不同的 API 差异:传递 input=None 的行为将与 input=b'' (或 input='',具体取决于其他参数) 一样而不是使用父对象的标准输入文件处理。

默认情况下,此函数将把数据返回为已编码的字节串。 输出数据的实际编码格式将取决于发起调用的命令,因此解码为文本的操作往往需要在应用程序层级上进行处理。

此行为可以通过设置 text, encoding, errors 或将 universal_newlines 设为 True 来重载,具体描述见 常用参数run()

要在结果中同时捕获标准错误,请使用 stderr=subprocess.STDOUT:

1
2
3
4
5
>>> subprocess.check_output(
··· "ls non_existent_file; exit 0",
··· stderr=subprocess.STDOUT,
··· shell=True)
'ls: non_existent_file: No such file or directory\n'

Added in version 3.1.

在3.3版本发生变更:

timeout 被添加

在3.4版本发生变更:

增加了对 input 关键字参数的支持。

在3.6版本发生变更:

增加了 encodingerrors。详情参见 run()

Added in version 3.7:

text 作为 universal_newlines 的一个更具可读性的别名被添加。

在3.12版本发生变更:

针对 shell=True 改变的 Windows shell 搜索顺序。 当前目录和 %PATH% 会被替换为 %COMSPEC%%SystemRoot%\System32\cmd.exe。 因此,在当前目录中投放一个命名为 cmd.exe 的恶意程序不会再起作用。

shlex — 简单的词法分析

shlex 类可用于编写类似 Unix shell 的简单词法分析程序。通常可用于编写“迷你语言”(如 Python 应用程序的运行控制文件)或解析带引号的字符串。

shlex 模块中定义了以下函数:

shlex.split(s, comments=False, posix=True)

用类似 shell 的语法拆分字符串 s。如果 comments 为 False (默认值),则不会解析给定字符串中的注释 (commenters 属性的 shlex 实例设为空字符串)。 本函数默认工作于 POSIX 模式下,但若 posix 参数为 False,则采用非 POSIX 模式。

在 3.12 版本发生变更: 传入 None 作为 s 参数现在会引发异常,而不是读取 sys.stdin


备注:将 shell 命令拆分为参数序列的方式可能并不很直观,特别是在复杂的情况下。 shlex.split() 可以演示如何确定 args 适当的拆分形式:

1
2
3
4
5
6
7
>>> import shlex, subprocess
>>> command_line = input()
/bin/vikings -input eggs.txt -output "spam spam.txt" -cmd "echo '$MONEY'"
>>> args = shlex.split(command_line)
>>> print(args)
['/bin/vikings', '-input', 'eggs.txt', '-output', 'spam spam.txt', '-cmd', "echo '$MONEY'"]
>>> p = subprocess.Popen(args) # Success!

特别注意,由 shell 中的空格分隔的选项(例如 -input)和参数(例如 eggs.txt )位于分开的列表元素中,而在需要时使用引号或反斜杠转义的参数在 shell (例如包含空格的文件名或上面显示的 echo 命令)是单独的列表元素。


argparse 教程

argparse 教程 — Python 3.12.5 文档

这篇教程旨在作为 argparse 的入门介绍,此模块是 Python 标准库中推荐的命令行解析模块。

备注:

还有另外两个模块可以完成同样的任务,即 [getopt](https://docs.python.org/zh-cn/3/library/getopt.html#module-getopt getopt: Portable parser for command line options; support both short and long option names.) (等价于 C 语言中的 getopt()) 和已被弃用的 [optparse](https://docs.python.org/zh-cn/3/library/optparse.html#module-optparse optparse: Command-line option parsing library.(已弃用))。 还要注意 [argparse](https://docs.python.org/zh-cn/3/library/argparse.html#module-argparse argparse: Command-line option and argument parsing library.) 是基于 [optparse](https://docs.python.org/zh-cn/3/library/optparse.html#module-optparse optparse: Command-line option parsing library.(已弃用)) 的,因此用法与其非常相似。

socket — 低层级的网络接口

socket — 低层级的网络接口 — Python 3.12.5 文档

这个模块提供了访问 BSD 套接字 的接口。在所有现代 Unix 系统、Windows、macOS 和其他一些平台上可用。

备注:一些行为可能因平台不同而异,因为调用的是操作系统的套接字API。

常量

socket.AF_UNIX

socket.AF_INET

socket.AF_INET6

这些常量表示地址(和协议)族,被用作传给 socket() 的第一个参数。 如果 AF_UNIX 常量未定义则该协议将不受支持。 根据具体系统可能会有更多的常量可用。

socket.SOCK_STREAM

socket.SOCK_DGRAM

socket.SOCK_RAW

socket.SOCK_RDM

socket.SOCK_SEQPACKET

这些常量表示套接字类型,被用作传给 socket() 的第二个参数。 根据具体系统可能会有更多的常量可用。 (只有 SOCK_STREAM 和 SOCK_DGRAM 是普遍适用的。)

套接字对象

socket.setsockopt(leveloptnamevalue: int)

socket.setsockopt(leveloptnamevalue: buffer)

socket.setsockopt(leveloptnameNoneoptlen: int)

设置给定套接字选项的值 (参见 Unix 手册页 setsockopt(2))。 所需的符号常量已定义在本模块中 (SO_* 等 )。 该值可以是整数、None 或表示缓冲区的 bytes-like object。 在后一种情况下将由调用者确保字节串中包含正确的数据位 (请参阅可选的内置模块 struct 了解如何将 C 结构体编码为字节串)。 当 value 设为 None 时,optlen 参数是必须的。 这等价于调用 setsockopt() C 函数并设置 optval=NULL 和 optlen=optlen

SO_REUSEADDR

Indicates that the rules used in validating addresses supplied in a bind(2) call should allow reuse of local addresses. For AF_INET sockets this means that a socket may bind, except when there is an active listening socket bound to the address. When the listening socket is bound to INADDR_ANY with a specific port then it is not possible to bind to this port for any local address. Argument is an integer boolean flag.

SOL_SOCKET

getsockopt(2) — manpages-dev — Debian bookworm — Debian Manpages

SYNOPSIS

#include <sys/socket.h>

int getsockopt(int sockfd, int level, int optname,
void optval[restrict .optlen],*
*socklen_t restrict optlen
);**
int setsockopt(int sockfd, int level, int optname,
const void optval[.optlen],
socklen_t optlen);

DESCRIPTION

getsockopt() and setsockopt() manipulate options for the socket referred to by the file descriptor sockfd. Options may exist at multiple protocol levels; they are always present at the uppermost socket level.

When manipulating socket options, the level at which the option resides and the name of the option must be specified. To manipulate options at the sockets API level, level is specified as SOL_SOCKET. To manipulate options at any other level the protocol number of the appropriate protocol controlling the option is supplied. For example, to indicate that an option is to be interpreted by the TCP protocol, level should be set to the protocol number of TCP; see getprotoent(3).

The arguments optval and optlen are used to access option values for setsockopt(). For getsockopt() they identify a buffer in which the value for the requested option(s) are to be returned. For getsockopt(), optlen is a value-result argument, initially containing the size of the buffer pointed to by optval, and modified on return to indicate the actual size of the value returned. If no option value is to be supplied or returned, optval may be NULL.

Optname and any specified options are passed uninterpreted to the appropriate protocol module for interpretation. The include file <sys/socket.h> contains definitions for socket level options, described below. Options at other protocol levels vary in format and name; consult the appropriate entries in section 4 of the manual.

Most socket-level options utilize an int argument for optval. For setsockopt(), the argument should be nonzero to enable a boolean option, or zero if the option is to be disabled.

For a description of the available socket options see socket(7) and the appropriate protocol man pages.

threading — 基于线程的并行

这个模块定义了许多类,详见以下部分。

该模块的设计基于 Java的线程模型。 但是,在Java里面,锁和条件变量是每个对象的基础特性,而在Python里面,这些被独立成了单独的对象。 Python 的 Thread 类只是 Java 的 Thread 类的一个子集;目前还没有优先级,没有线程组,线程还不能被销毁、停止、暂停、恢复或中断。 Java 的 Thread 类的静态方法在实现时会映射为模块级函数。

线程对象

Thread 类代表一个在独立控制线程中运行的活动。 指定活动有两种方式:向构造器传递一个可调用对象,或在子类中重载 run() 方法。 其他方法不应在子类中重载(除了构造器)。 换句话说,只能 重载这个类的 __init__() 和 run() 方法。

当线程对象一旦被创建,其活动必须通过调用线程的 start() 方法开始。 这会在独立的控制线程中发起调用 run() 方法。

一旦线程活动开始,该线程会被认为是 ‘存活的’ 。当它的 run() 方法终结了(不管是正常的还是抛出未被处理的异常),就不是’存活的’。 is_alive() 方法用于检查线程是否存活。

其他线程可以调用一个线程的 join() 方法。这会阻塞调用该方法的线程,直到被调用 join() 方法的线程终结。

线程有名字。名字可以传递给构造函数,也可以通过 name 属性读取或者修改。

如果 run() 方法引发了异常,则会调用 threading.excepthook() 来处理它。 在默认情况下,threading.excepthook() 会静默地忽略 SystemExit

一个线程可以被标记成一个“守护线程”。 这个标识的意义是,当剩下的线程都是守护线程时,整个 Python 程序将会退出。 初始值继承于创建线程。 这个标识可以通过 daemon 特征属性或者 daemon 构造器参数来设置。

备注:守护线程在程序关闭时会突然关闭。他们的资源(例如已经打开的文档,数据库事务等等)可能没有被正确释放。如果你想你的线程正常停止,设置他们成为非守护模式并且使用合适的信号机制,例如: Event

有个 “主线程” 对象;这对应Python程序里面初始的控制线程。它不是一个守护线程。

创建“虚拟线程对象”是有可能的。 它们是与“外部线程”相对应 的线程对象,是在 threading 模块之外启动的控制线程,例如直接来自 C 代码。 虚拟线程对象的功能是受限的;它们总是会被视为处于激活和守护状态,且无法被 合并。 它们绝不会被删除,因为检测外部线程的终结是不可能做到的。

class threading.Thread(group=Nonetarget=Nonename=Noneargs=()kwargs={}, *, daemon=None)

应当始终使用关键字参数调用此构造函数。 参数如下:

group 应为 None;保留给将来实现 ThreadGroup 类的扩展使用。

target 是用于 run() 方法调用的可调用对象。默认是 None,表示不需要调用任何方法。

name 是线程名称。 在默认情况下,会以 “Thread-N“ 的形式构造唯一名称,其中 N 为一个较小的十进制数值,或是 “Thread-N (target)” 的形式,其中 “target” 为 target.__name__,如果指定了 target 参数的话。

args 是用于发起调用目标函数的参数列表或元组。 默认为 ()

kwargs 是用于调用目标函数的关键字参数字典。默认是 {}

如果不是 Nonedaemon 参数将显式地设置该线程是否为守护模式。 如果是 None (默认值),线程将继承当前线程的守护模式属性。

如果子类型重载了构造函数,它一定要确保在做任何事前,先发起调用基类构造器(Thread.__init__())。

在 3.3 版本发生变更: 增加了 daemon 形参。

在 3.10 版本发生变更: 使用 target 名称,如果 name 参数被省略的话。

start()

开始线程活动。

它在一个线程里最多只能被调用一次。 它安排对象的 run() 方法在一个独立的控制线程中被调用。

如果同一个线程对象中调用这个方法的次数大于一次,会抛出 RuntimeError 。

run()

代表线程活动的方法。

你可以在子类型里重载这个方法。 标准的 run() 方法会对作为 target 参数传递给该对象构造器的可调用对象(如果存在)发起调用,并附带从 args 和 kwargs 参数分别获取的位置和关键字参数。

使用列表或元组作为传给 Thread 的 args 参数可以达成同样的效果。

示例:

1
2
3
4
5
6
7
>>> from threading import Thread
>>> t = Thread(target=print, args=[1])
>>> t.run()
1
>>> t = Thread(target=print, args=(1,))
>>> t.run()
1

textwrap — 文本自动换行与填充

textwrap — 文本自动换行与填充 — Python 3.12.5 文档