第2章:网络编程——socket库

发布于 2021-08-31  338 次阅读


TCP客户端及UDP客户端、服务端

基础的TCP客户端及UDP客户端、服务端等简单的代码块就不再进行展示,写一些学习这部分内容时候的一些笔记吧。

  1. socket库已经集成在Python中,可能这句话不太准确,在IDE软件中编程时,import部分操作可以在编程过程中自动进行补全,不过在具体使用过程中,有时候会出现失效的情况,所以还是手动进行import操作最好。
  2. 一些函数:

socket(int socket_family,int socket_type,int protocol)
​  创建socket对象,后续对对象进行操作,之后有对socket()的部分详解。
​socket.connect(address)
 ​ 主动初始化TCP服务器连接,。一般address的格式为元组(hostname,port),如果连接出错,返回socket.error错误。
​socket.send(string)
​  送TCP数据,将string中的数据发送到连接的套接字。返回值是要发送的字节数量,该数量可能小于string的字节大小。
​socket.recv(bufsize)
​  收TCP数据,数据以字符串形式返回,bufsize指定要接收的最大数据量。flag提供有关消息的其他信息。
​  接收数据这一部分,较多时间应该是不能清晰明确接收所数据大小,所以使用while循环进行接收,配合其他操作也可以进行对某一终端进行监听等动作,目前是只进行了监听操作的测试。
​socket.sendto(address)
​  送UDP数据,将数据发送到套接字,address是形式为(ipaddr,port)的元组,指定远程地址。返回值是发送的字节数。
​socket.recvfrom(bufsize)
​  收UDP数据,与recv()类似,但返回值是(data,address)。其中data是包含接收数据的字符串,address是发送数据的套接字地址。
​  用方法:data,addr = socket.recvfrom(bufsize)
​socket.close()
​  闭套接字
​socket.bind(address)
​  定地址(host,port)到套接字, 在AF_INET下,address以元组(host,port)的形式表示地址。
​  建服务端socket套接字
​socket.listen(backlog)
​  始TCP监听。backlog指定在拒绝连接之前,操作系统可以挂起的最大连接数量。该值至少为1。
​socket.accept()
​  动接受TCP客户端连接,(阻塞式)等待连接的到来

netcat.py

  这个脚本算是我进行Python学习中第一个较大体量的工具脚本,这个学习过程怎么说呢,很痛苦,因为对Python很多东西都不了解就开始功能较多的工具编写,所以写的比较艰难,再加上顶着Python2 to Python3的语言版本差异,这部分的学习一直持续了两周多时间。
  不过,也很感谢我这种比较莽的学习方式:先学,在学习过程遇到的不会的、不懂的再去补充学习。这样的方法学习起来会很有效率,问题的产生到解决问题这种过程的印象很深刻,所以在这个脚本之后我基本上就可以开始独立编写一部分的Poc脚本。

import sys
import socket
import getopt
import threading
import subprocess

# 全局变量
listen = False
command = False  # 命令
upload = False
execute = ""  # 执行
target = ""  # 连接目标
upload_destination = ""  # 上传目的路径
port = 0

def main():
    global listen  # global用来在函数中声明此变量是全局变量
    global port
    global execute
    global command
    global upload_destination
    global target

    if not len(sys.argv[1:]):
        usage()
    try:
        opts,args = getopt.getopt(sys.argv[1:],"hle:t:p:cu:",["help","listen","execute","target","port","command","upload"])
    except getopt.GetoptError as err:
        print(str(err))
        usage()

    for o,a in opts:  # 判断输入的参数是否存在,并对变量进行赋值
        if o in ("-h","--hlep"):
            usage()
        elif o in ("-l","--listen"):
            listen = True
        elif o in ("-e","--execute"):
            execute = a
        elif o in ("-c","--command"):
            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"  # assert 表达式 [, 参数],返回值为假时,则执行后面的异常

    if not listen and len(target) and port > 0:  # 判断时进行监听还是发送数据,listen的值与目标的地址和端口是否设置的条件
        # 不进行监听,连接远程主机
        # buffer = sys.stdin.read()  # 相当于input()
        buffer = "test"
        client_sender(buffer)

    if listen:
        server_loop()       # 监听端口

def usage():
    print("Usage:nctest.py -t target_host -p port")
    print("-l --listen                 - listen on [host]:[port] for incoming connections")
    print("-e --execute=file_to_run    - execute the given file upon receiving a connection")
    print("-c --command                - initialize a command shell")
    print("-u --upload=destination     - upon receiving connection upload a file and write to [destination]")
    print("")
    print("")
    print("Examples:")
    print("nctest.py -t 192.168.0.1 -p 5555 -l -c")
    print("nctest.py -t 192.168.0.1 -p 5555 -l -u=c:\\target.exe")
    print("nctest.py -t 192,168.0.1 -p 5555 -l -e=\"cat /etc/passwd\"")
    print("echo 'ABCDEFGHI' | ./nctest.py -t 192.168.11.12 -p 135")
    sys.exit(0)

def client_sender(buffer):
    client = socket.socket(socket.AF_INET,socket.SOCK_STREAM)

    try:
        client.connect((target,port))

        if len(buffer):
            client.send(buffer.encode())

        while True:
            recv_len = 1
            response = ""

            while recv_len:  # 接收目标主机发送回来的数据,并赋值给response变量,
                data = client.recv(4096).decode("gb2312")
                recv_len = len(data)  # 此处取data的长度仅为了下面IF的判断结束循环,recv_len为1,即永真
                response += data

                if recv_len < 4096:
                    break

            print(response)
            buffer = input("")  # 此处为是否有更多的需要服务端响应的“请求”
            buffer += "\n"
            client.send(buffer.encode())
    except:
        print("[*] Exception Exiting.")
        client.close()

def server_loop():
    global target
    if not len(target):
        target = "0.0.0.0"
    server = socket.socket(socket.AF_INET,socket.SOCK_STREAM)
    server.bind((target,port))
    server.listen(5)
    while True:
        client_socket,addr = server.accept()
        client_tread = threading.Thread(target=client_handler,args=(client_socket,))
        client_tread.start()

def run_command(command):
    command = command.rstrip()  # 删除字符串后的特定字符,默认为空格

    try:
        output = subprocess.check_output(command,stderr=subprocess.STDOUT,shell=True)
    except:
        output = "Failed to execute command.\r\n"

    return output

def client_handler(client_socket):
    global upload
    global execute
    global command

    if len(upload_destination):
        file_buffer = ""

        while True:
            data = client_socket.recv(4096)

            if not data:
                break
            else:
                file_buffer += data

        try:
            file_descriptor = open(upload_destination,"wb")
            file_descriptor.write(file_buffer.encode())
            file_descriptor.close()
            # 确认操作
            client_socket.send("Successfully saved file to %s\r\n" % upload_destination)
        except:
            client_socket.send("Failed to save file to %s\r\n" % upload_destination)

    if len(execute):
        output = run_command(execute)
        client_socket.send(output)

    if command:

        while True:

            windows = "<BHP:#>"
            client_socket.send(windows.encode())
            cmd_buffer = ""                 # 接收输入的命令
            while "\n" not in cmd_buffer:
                cmd_buffer = client_socket.recv(4096)
                cmd_buffer = cmd_buffer.decode()
                cmd_buffer += cmd_buffer

            response = run_command(cmd_buffer)
            response_type = str(type(response))
            if "str" in response_type:
                client_socket.send(response.encode())
            elif "bytes" in response_type:
                client_socket.send(response)
            else:
                print("[*] Command response type Unresolved")
main()

# 调试命令:
    # server端:python netcat.py -t 127.0.0.1 -p 10086 -l -c
    # client端:python netcat.py -t 127.0.0.1 -p 10086

netcat.py的一些笔记

sys.srgv[]

  sys.argv[]是用来获取命令行输入内容的方法
  sys.argv[]返回的是一个列表,sys.argv[0]是程序本身的名字,所以最终结果要从第二位开始取,即sys.argv[1]

opts,args = getopt.getopt(sys.argv[1:],"hle:t:p:cu:",["help","listen","execute","target","port","command","upload"])

  使用sys.argv[1:]过滤掉第一个参数(它是执行脚本的名字,不应算作参数的一部分)
  使用短格式分析串"hle:"
  当一个选项只是表示开关状态时,即后面不带附加参数时,在分析串中写入选项字符
  当选项后面是带一个附加参数时,在分析串中写入选项字符同时后面加一个":"号。所以"hle:"就表示"hl"是开关选项;"e:"则表示后面应该带一个参数
  在命令行中输入方式短格式分析串举例:用友beanshell命令执行漏洞.py -h -l -e file(h和l为开关选项,e为需要附加参数,附加参数为:file)
  使用长格式分析串列表:["help","listen","execute","target","port","command","upload"]
  长格式串也可以有开关状态,即后面不跟"="号
  如果跟一个等号则表示后面还应有一个参数。这个长格式表示"help"等是一个开关选项;如果存在如"port="则表示后面应该带一个参数
  在命令行中输入方式短格式分析串举例:netcat.py --help --port=80(help为开关选项,port为需要附加参数,附加参数为:80)
  如果长短格式都存在的话,是否有附加参数仅定义短格式即可,在使用过程中长格式可以沿用短格式的参数输入方式
  调用getopt函数。函数返回两个列表:opts和args。opts为分析出的格式信息。args为不属于格式信息的剩余的命令行参数, 即不是按照getopt()
  里面定义的长或短选项字符和附加参数以外的信息。opts是一个两元素元组的列表。每个元素为:(选项串, 附加参数)。如果没有附加参数则为空串''
  而args则为:['file1', 'file2'],这就是上面不属于格式信息的剩余的命令行参数

output = subprocess.check_output(command,stderr=subprocess.STDOUT,shell=True) output = subprocess.check_output(command,stderr=subprocess.STDOUT,shell=True) subprocess.run(args,*,stdin=None,input=None,stdout=None,stderr=None,capture_output=False,shell=False,cwd=None,timeout=None,check=False,encoding=None,errors=None,text=None,env=None,universal_newlines=None)

  "command"此处参数名称为args,表示为需要执行的命令,必须是字符串或参数列表,可以不输入"args="
  args:表示要执行的命令。必须是一个字符串,字符串参数列表
  stdin、stdout和stderr:子进程的标准输入、输出和错误
   其值可以是subprocess.PIPE、subprocess.DEVNULL、一个已经存在的文件描述符、已经打开的文件对象或者None
   subprocess.PIPE表示为子进程创建新的管道
   subprocess.DEVNULL表示使用os.devnull
   默认使用的是None,表示什么都不做。另外,stderr可以合并到stdout里一起输出
  timeout:设置命令超时时间。如果命令执行时间超时,子进程将被杀死,并弹出TimeoutExpired异常
  check:如果该参数设置为True,并且进程退出状态码不是0,则弹出CalledProcessError异常
  encoding: 如果指定了该参数,则stdin、stdout和stderr可以接收字符串数据,并以该编码方式编码。否则只接收bytes类型的数据
  shell:如果该参数为True,将通过操作系统的shell执行指定的命令

其他库的一些函数

sys.stdin.read()
  相当于input()

Python2 to Python3

这一部分简单写一些Python2 to Python3的改变。
  数据类型上:

  1. python2和python3的区别最大的是在库的所能处理的数据的数据类型上,socket库中send和recv的数据都是bytes类型的,所以在send前需要str to bytes,recv后需要bytes to str,具体的情况下需要具体的处理方式,数据类型转换的方法、语句也有所不同。
  2. python3中的字符型数据类型为str和bytes,而python2中的字符数据类型为str和unicode,所以在py2脚本中isinstance()这个函数使用时,会出现unicode类型的判断。

  部分函数上:
  函数这部分呢,仅代表我个人观点,对一些Python2中使用但在Python3中不使用或少使用的函数都定义为语言版本之间的不同
   isinstance() 和 type()
     isinstance(object, classinfo):判断一个对象是否是一个已知的类型
     type():返回对象的类型
   xrange() 和 range()
     xrange(start,stop[,step]),用法、作用同range(),python3中没有xrange()这个函数
  代码编写上:

  1. isinstance()这个函数在python3中个人认为基本上没有太大的作用,可以使用type()构造判断语句,虽然代码相对会多一些,但是稳定
  2. py3的占位符,占位符与C语言和py2会存在一定的区别,注意写法

socket库

socket.socket(int socket_family,int socket_type,int protocol)

int socket_family:
  创建的socket的地址簇或者协议簇,取值以AF或PF开头,实际使用中两者并没有区别
  最常用的取值:AF_INET、AF_PACKET、AF_UNIX等

  AF_UNIX:主要用于主机内部进程间通信
  AF_INET与AF_PACKET区别在于前者只能看到三层以上的内容,后者可以看到二层
  AF_INET是与IP报文对应的,而AF_PACKET则是与Ethernet II报文对应的。AF_INET创建的套接字称为inet socket,而AF_PACKET创建的套接字称为packet socket

int socket_type:

socket_family会影响socket_type和protocol取值范围
socket_type表示套接字类型:
 enum sock_type{
  SOCK_STREAM = 1,
  SOCK_DGRAM = 2,
  SOCK_RAW = 3,
 };

  SOCK_STREAM 流格式套接字

  基于TCP协议,是一种可靠的、双向的通信数据流,数据可以准确无误地到达另一台计算机,如果有损坏或丢失,可以重新发送
  SOCK_STREAM的几个特征:
    数据在传输过程中不会消失
    数据是按照顺序传输的
    数据的发送和接收不是同步的
  流格式套接字的内部有一个缓冲区(也就是字符数组),通过 socket 传输的数据将保存到这个缓冲区。接收端在收到数据后并不一定立即读取,只要数据不超过缓冲区的容量,接收端有可能在缓冲区被填满以后一次性地读取,也可能分成好几次读取
  流格式套接字有什么实际的应用场景吗?浏览器所使用的 http 协议就基于面向连接的套接字,因为必须要确保数据准确无误,否则加载的 HTML 将无法解析

  SOCK_DGRAM** 数据报格式套接字

  基于UDP协议,也叫"无连接的套接字",是一种不可靠的、不按顺序传递的、以追求速度为目的的套接字,只管传输数据,不做数据校验,数据损坏和丢失的情况下无法进行补救,但传输效率比刘格式套接字要高
  SOCK_DGRAM的几个特征:
    强调快速传输而非传输顺序
    传输的数据可能丢失也可能损毁
    限制每次传输的数据大小
    数据的发送和接收是同步的
  SOCK_STREAM和SOCK_DGRAM统称为标准套接字

  SOCK_RAW 原始套接字

  SOCK_RAW可以处理普通套接字无法处理的ICMP、IGMP等网络报文,其次,SOCK_RAW可以处理特殊的IPv4报文,此外利用原始套接字,可以通过IP_HDRINCL套接字选项有用户构造IP头
  SOCK_RAW可以处理普通的网络报文之外,还可以处理一些特殊协议报文以及操作IP层及其以上的数据
  SOCK_RAW的几个特征
  若设置IP_HDRINCL选项,SOCK_RAW可以操作IP头数据,即用户需要甜筒IP头及其以上的payload,否则SOCK_RAW无法操作IP头数据
  端口对于SOCK_RAW是没有意义的
  如果使用bind函数绑定本地IP,那么如果IP_HDRINCL未设置,则需要用此IP填充源IP地址,若不调用bind则源IP地址设置为外出接口的主IP地址
  如果使用connect函数设置目标IP,则可以使用send或write函数发送报文,而不需要使用sendto函数
  内核处理流程:
    接收到的TCP、UDP分组不会传递给任何SOCK_RAW
    ICMP、IGMP报文分组传递给SOCK_RAW
    内核不识别的IP报文传递给SOCK_RAW
  SOCK_RAW是否接收报文:
    protocol指定类型需要匹配,否则不传递给该SOCK_RAW
    如果使用bind函数绑定了源IP,则报文目的IP必须和绑定的IP匹配,否则不传递给该SOCK_RAW
    如果使用了connect函数绑定了目的IP,则报文源IP必须和制定的IP匹配,否则不传递给该SOCK_RAW
  SOCK_RAW的应用场景
    原始套接字处理的只是IP层及其以上的数据,比如实现SYN FLOOD攻击、处理PING报文等。当需要操作更底层的数据的时候,就需要采用其他的方式

int protocol:

  protocol表示套接字上报文的协议
  默认值为0
  对于AF_INET地址簇,protocol的取值范围是如IPPROTO_TCP、IPPROTO_UDP、IPPROTO_ICMP这样的报文协议类型,或者IPPROTO_IP=0这个特殊值
  对于AF_PACKET地址簇,protocol的取值范围是ETH_P_IP、ETH_P_ARP这样的以太帧协议类型
  socket的协议开关
  每一个inet socket只能解法一种IP协议类型的报文,例如TCP套接字不能接收UDP报文,反之亦同
  同时,protocol的值还收到socket_type的限制,不匹配的取值会导致套接字创建操作返回失败

socket.setsockopt(level,optname,value)

​功能作用:

​获取(get)或者设置(set)与某个套接字关联的选项

参数含义:

​  level定义了那个选项将被使用,通常情况下是SOL_SOCKET,意思是正在使用的socket选项,也可以通过设置一个特殊协议号码来设置协议选项
​  optname提供使用的特殊选项,

  s.setsockopt(level,optname,value) 设置给定套接字选项的值。
  s.getsockopt(level,optname[.buflen]) 返回套接字选项的值。

写在最后

  这本书第2章后续是ssh和proxy工具的编写,并没有太多的新的东西,所以就不再对此部分写读书笔记了,主要还是熟悉这些三方库的代码语法和版本语言的差异,也逐渐养成自己的代码编写习惯。
  总的来说,这块的学习最大的收获就是对程序dubug相关的操作和经验,插入代码桩、try-except结构的使用及优化对应报错信息的输出、IDE软件的DeBug模块的使用等等。
  netcat.py因为是我第一个编写的脚本,写的还比较的粗糙,dubug的代码桩还没有清除干净,虽然是抄的书,但是还是可以看出一些个人习惯,也依据自己的理解对脚本做了一些小小的优化,比如说部分代码的结构、输入输出、回显优化等等
  另外,脚本中,仅调试了command模块,不同命令的response回显的数据类型是不同的,需要进行对数据类型的判断,对此做了部分优化,除此之外发现有些命令仍然不能正常使用,没有具体检测是哪些,应该是与脚本执行的权限相关,不过脚本的基础功能是可以实现了,有道是脚本只要能跑,咱就不动了!