《Python 黑帽子:黑客与渗透测试编程之道》笔记一
专栏开了很久,也一直想写点东西,可实在是太懒,于是托啊托到了现在。最近微有时间,所以想琢磨着写点简单明了的小文章。刚好准备整理一下以前读过的书籍,于是决定写点读书笔记。随手拿起了这本,希望能一起讨论。
关于此书的简介和章节目录直接略过,直接开始讨论正文。
第一章 设置 Python 环境
简要交代一下,作者是在 VMWare Player 中安装了 32 位的 Kali-linux-1.0.9 的系统且自带 Python 2.7.3,IDE 选择的是 WingIDE5。我安装的操作系统是 Kali GNU/Linux Rolling (Kali 2017.2)。关于 IDE,因为习惯了 PyCharm 所以依旧采用它。最后有关 Python 版本的问题,在代码讲解环节尽量用 Python3 去重写书中代码,具体是 Python 3.5.4。
第二章 网络基础
正如书中所讲,如果你已经进入了目标企业的内部网络,那么就有可能会陷入某种困境:比如你找不到任何用以发动工具的工具,没有 netcat、Wireshark、Nmap等等,甚至连编译器都没有。不过在大多情况下,你会发现目标环境中安装了 Python,是不是很惊喜很意外!这也将是你后续工作的起点。
本章介绍的,也是一些使用 Socket 模块进行网络编程的基本方法,学会编写基本的客户端、服务端、TCP 代理并完善为我们自己的 netcat,最后完成一个 shell 工具的编写。本章也是后续章节的基础,后续章节中我们会学习编写主机发现工具,实现跨平台嗅探,创建一个远程木马框架以及其他的一些小技巧。
1、Python 网络编程简介
开发人员可以使用大量的第三方 Python 工具创建网络客户端和服务器,这些第三方工具的核心模块是 socket 模块。这个模块展示了快速创建 TCP 和 UDP 服务器及客户端、使用原始套接字等必需的代码。为了攻击进入或者保持控制目标主机,socket 模块是我们必须使用的模块。让我们从创建一个简单的客户端和服务器开始,这会是你将来最常编写的两个网络脚本。
2、TCP 客户端
在渗透测试过程中,我们会经常性的创建一个 TCP 客户端来连接服务、发送垃圾数据、进行模糊测试等等。如果你工作在一个独立的大型企业网络环境中,那么你基本不会有大量的工具以及编译器,也不可能访问互联网。这时候就有必要手动撸一个 TCP 客户端出来了,下面是书中的代码:
import socket
target_host = "www.google.com"
target_port = 80
# 建立一个 socket 对象
client = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
# 连接客户端
client.connect((target_host, target_port))
# 发送一些数据
client.send("GET / HTTP/1.1\r\nHost: google.com\r\n\r\n")
# 接收一些数据
response = client.recv(4096)
print response
这段代码简单的模拟了浏览器的行为,给 Google 发送了一段请求报文,并且打印收到的响应信息。下面用 Python3 重写一遍,这次向百度发送请求报文,并做一些修改:
import socket
target_host = "www.baidu.com"
target_port = 80
client_socket = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
client_socket.connect((target_host, target_port))
client_socket.send(bytes("GET / HTTP/1.1\r\nHost: www.baidu.com\r\n\r\n", "utf-8"))
# client_socket.send("GET / HTTP/1.1\r\nHost: www.baidu.com\r\n\r\n".encode("utf-8"))
# 接收响应数据
response = client_socket.recv(4096)
print("response: ", response)
client_socket.close()
打开浏览器,地址栏输入“www.baidu.com”回车进入百度首页,然后 F12 进入调试页面,在 Network 选项中可以查看对应的 Request Headers 和 Response Headers 详细信息。其中 Request Headers 的详情如下:
上图中箭头所示就是我们构造的内容,行与行之间借助“\r\n”实现换行。报文最后用“\r\n\r\n”是因为请求报文与信息之间是以空行隔开的。在此程序中我们构造了一条最简单的报文然后用 send() 函数发送出去。有两种发送方式:
client_socket.send(bytes("GET / HTTP/1.1\r\nHost: www.baidu.com\r\n\r\n", "utf-8"))
或者:
client_socket.send("GET / HTTP/1.1\r\nHost: www.baidu.com\r\n\r\n".encode("utf-8"))
运行程序,可得如下结果:
response: b'HTTP/1.1 200 OK\r\nDate: Sat, 23 Sep 2017 10:42:23 GMT\r\nContent-Type: text/html\r\nContent-Length: 14613\r\nLast-Modified: Wed, 20 Sep 2017 09:59:00 GMT\r\nConnection: Keep-Alive\r\nVary: Accept-Encoding\r\nSet-Cookie: BAIDUID=111FE40F374629CC169DC43AB1E4FC6E:FG=1; expires=Thu, 31-Dec-37 23:55:55 GMT; max-age=2147483647; path=/; domain=.baidu.com\r\nSet-Cookie: BIDUPSID=111FE40F374629CC169DC43AB1E4FC6E; expires=Thu, 31-Dec-37 23:55:55 GMT; max-age=2147483647; path=/; domain=.baidu.com\r\nSet-Cookie: PSTM=1506163343; expires=Thu, 31-Dec-37 23:55:55 GMT; max-age=2147483647; path=/; domain=.baidu.com\r\nP3P: CP=" OTI DSP COR IVA OUR IND COM "\r\nServer: BWS/1.1\r\nX-UA-Compatible: IE=Edge,chrome=1\r\nPragma: no-cache\r\nCache-control: no-cache\r\nAccept-Ranges: bytes\r\n\r\n'
同样的,在浏览器中,我们看到的 Response Headers 信息如下:
各条目的意义不在做详细的解释。
倘若我们只想发送几句简单的问候语给对方,该怎样编写客户端并测试呢?此时可借助 NetAssist 工具作服务端辅助完成测试。
NetAssist 所在环境为 Windows,IP 是 192.168.1.102。Kali 的 IP 为 192.168.1.104。客户端代码如下:
import socket
target_host = "192.168.1.102"
target_port = 6688
# 建立 socket 对象
client_socket = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
# target_host 和 target_port 以元组的形式传进 connect()
client_socket.connect((target_host, target_port))
# 采用 gb2312 的编码格式是因为要发送中文的话 NetAssist 只能用 gb2312 解码。
client_socket.send("你吼哇,我的朋友!".encode("gb2312"))
# 接收从 NetAssist 发送的数据并打印出来。
recv_data = client_socket.recv(1024)
print("recv_data: %s" % recv_data)
client_socket.close()
NetAssist 接收到数据之后给客户端一个回馈,结果如下:
NetAssist 将 "I'm fine, and you?" 发送至客户端,客户端顺利接收:
上述代码中,有关 socket 模块的细节不做介绍了,Google 一下资料很多的。就这两个小例子来说,我们为了方便假设所有的连接都是成功的,并未考虑出错或者异常的情况。
3、UDP 客户端
Python 编写 UDP 客户端的套路和 TCP 客户端相差不是很大,只需简单的修改即可。书中的代码如下:
import socket
target_host = "127.0.0.1"
target_port = 80
# 建立一个 socket 对象
client = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
# 发送一些数据
client.sendto("AAABBBCCC", (target_host, target_port))
# 接收一些数据
data, addr = client.recvfrom(4096)
print data
需注意的地方是,创建 UDP 套接字的时候要把套接字类型改为 SOCK_DGRAM,发送数据的函数不再是 send() 而是 sendto(),接受数据的函数不再是 recv() 而是 recvfrom()。我们不在改写这个 UDP 客户端程序,单独写一个接收数据的 UDP 服务端,其代码如下:
import socket
# 监听任意地址
target_host = ""
target_port = 6688
udp_socket = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
udp_socket.bind((target_host, target_port))
while True:
recv_data = udp_socket.recvfrom(1024)
print("[%s]:%s" % (str(recv_data[1]), recv_data[0].decode("gb2312")))
依旧借助 NetAssist 工具来调试,发送四句话,具体情况如下:
UDP 服务端接收数据情况如下:
4、TCP 服务器
用 Python 创建 TCP 服务端和创建客户端一样简单。我们可能需要将自己的 TCP 服务端绑定到 shell 或者创建一个代理(后续章节再做讲解)。首先,我们先创建一个标准的多线程 TCP 服务器,书中代码如下:
import socket
import threading
bind_ip = "0.0.0.0"
bind_port = 9999
server = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
server.bind((bind_ip, bind_port))
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 = server.accept()
print "[*] Accepted connection from: %s:%d" % (addr[0], addr[1])
# 挂起客户端线程,处理传入的数据
client_handler = threading.Thread(target=handle_client(), args=(client,))
client_handler.start()
当有客户端试图连接服务端的时候,程序进入 while 循环。在 while 中会将客户端的套接字保存到 client 变量,客户端的 IP 和 端口保存到 addr 变量中,所以下面 print 打印出来的信息就是客户端的 IP 和 端口。
随后以 handle_client() 为回调函数创建一个新的线程对象 client_handler,并将客户端的套接字 client 作为句柄传递给了新创建的这个线程对象,然后启动线程进入 handle_client() 函数执行,同时 handle_client() 接受一个参数,其实就是当作句柄传入线程的 client。
下面我们自己实现一个相对比较健壮的 TCP 服务端,代码如下:
from socket import *
from threading import Thread
# 处理客户端的请求
def handle_client(client_socket, client_addr):
while True:
recv_data = client_socket.recv(1024)
if len(recv_data) > 0:
print("[*] recv_data [%s]:%s" % (str(client_addr), recv_data.decode("gb2312")))
client_socket.send(bytes("收到!", "gb2312"))
else:
print("[*] [%s] 客户端已断开!" % str(client_addr))
break
client_socket.close()
def main():
bind_ip = "0.0.0.0"
bind_port = 8888
server_socket = socket(AF_INET, SOCK_STREAM)
# 实现端口复用
server_socket.setsockopt(SOL_SOCKET, SO_REUSEADDR, 1)
server_socket.bind((bind_ip, bind_port))
server_socket.listen(5)
try:
while True:
# 等待新的客户端
client_socket, client_addr = server_socket.accept()
print("[*] 客户端连接成功:%s:%d"% (client_addr[0], client_addr[1]))
client_handle = Thread(target=handle_client, args=(client_socket, client_addr))
client_handle.start()
# 线程中是共享 client_socket 这个套接字的,所以此时执行 client_socket.close() 操作的话
#会导致 client_socket 不在可用而出现错误
finally:
server_socket.close()
if __name__ == '__main__':
main()
运行结果如下:
多进程 TCP 服务端的一种实现方式:
from socket import *
from multiprocessing import *
# 处理客户端的请求
def handle_client(client_socket,client_addr):
while True:
recv_data = client_socket.recv(1024)
if len(recv_data) > 0:
print("[*] recv_data [%s]:%s" % (str(client_addr), recv_data.decode("utf-8")))
client_socket.send(bytes("收到!", "gb2312"))
else:
print("[*] [%s]客户端已经关闭" % str(client_addr))
break
client_socket.close()
def main():
bind_ip = "0.0.0.0"
bind_port = 8888
server_socket = socket(AF_INET, SOCK_STREAM)
server_socket.setsockopt(SOL_SOCKET, SO_REUSEADDR, 1)
server_socket.bind((bind_ip, bind_port))
server_socket.listen(5)
try:
while True:
client_socket, client_addr = server_socket.accept()
print("[*] 客户端连接成功:%s:%d" % (client_addr[0], client_addr[1]))
client_handle = Process(target=handle_client, args=(client_socket, client_addr))
client_handle.start()
# 子进程 copy 了一份父进程的资源,所以可以对 client_socket 进行 close 操作。
client_socket.close()
finally:
server_socket.close()
if __name__ == '__main__':
main()
多进程 TCP 服务端不再放测试截图。
书中下一个知识点是“取代 netcat”,下篇笔记中详细讨论。
PS: 不出意外的话这个专栏只写读书笔记,努力保持一个礼拜一篇,如果条件允许,会记录一些比较有意义的渗透经历。
文中如有错误,还请斧正!