vlambda博客
学习文章列表

一张一弛之Websocket攻防总结


概念


      为了解决web应用中数据实时交互问题,websocket应运而生,与轮询方式不同它是html5提供的一种基于http请求完成握手、tcp协议建立持久化连接实现全双工通讯的技术,过程如下:

一张一弛之Websocket攻防总结

01

通过http协议向服务端发送协议升级请求


  
    
    
  
GET /ws HTTP/1.1 Sec-WebSocket-Key: VfKuQG4BzhBYkNvraRlHUA== Connection: keep-alive, Upgrade Upgrade: websocket

02

服务端返回状态码101,同意升级


  
    
    
  
HTTP/1.1 101 Switching Protocols Upgrade: websocket Connection: Upgrade Sec-WebSocket-Accept: mjBz+76GMZxXOSkefWtoVHlPhU0=

03

传输数据


  
    
    
  
ws.onopen=function () { ws.send("你好"); }


实现


2.1 客户端


01

创建对象


var Socket = new WebSocket(url, [protocol] );

02

该对象的4个事件


事件
事件处理程序
描述
open
Socket.onopen
连接建立时触发
message
Socket.onmessage
客户端接收服务端数据时触发
error
Socket.onerror
通信发生错误时触发
close
Socket.onclose
连接关闭时触发

Socket.onopen=function() { console.log("websocket状态:"+Socket.readyState);};
Socket.onmessage=function(aaa){ console.log("收到消息:"+ aaa.data); };
Socket.onclose=function() { console.log("连接已关闭");};

03

该对象的1个属性


属性
描述
Socket.readyState
0:正在连接中
1:已经连接并且可以通讯
2:连接正在关闭
3:连接已关闭或者没有连接成功

var readyState =Socket.readyState;

04

该对象的2个方法


方法
描述
Socket.send()
发送数据
Socket.close()
关闭连接

Socket.onopen= function () { Socket.send('hello'); //发送数据  Socket.close();};

2.2 服务端


      不同服务器语言有对应模块支持websocket服务,python中可以使用socket模块开启监听,等待客户端连接,过程如下:

一张一弛之Websocket攻防总结
 

01

设置socket监听,等待连接


# 创建套接字监听
sock = socket.socket()sock.setsockopt(socket.SOL_SOCKET,socket.SO_REUSEADDR,1)sock.bind(("127.0.0.1",8080))sock.listen(5)
# 等待用户连接
conn,addr = sock.accept()data = conn.recv(8096)

02

准备返回包


response_tpl ="HTTP/1.1101 Switching Protocols\r\n" \ "Upgrade:websocket\r\n" \ "Connection: Upgrade\r\n" \ "Sec-WebSocket-Accept: %s\r\n" \magic_string ='258EAFA5-E914-47DA-95CA-C5AB0DC85B11'value = headers['Sec-WebSocket-Key'] +magic_string
# 计算Sec-WebSocket-Accept值
ac = base64.b64encode(hashlib.sha1(value.encode('utf-8')).digest())
# 发送返回包,响应请求
response_str = response_tpl % (ac.decode('utf-8'))conn.send(bytes(response_str, encoding='utf-8'))

03

处理收到的数据

        服务端采用Tcp Socket方式接收数据,格式如下:

一张一弛之Websocket攻防总结

Mask: 1个比特
  • 表示是否要对数据载荷进行掩码操作

  • 从客户端向服务端发送数据时,需要对数据进行掩码操作

  • 从服务端向客户端发送数据时,不需要对数据进行掩码操作


Payload length:7位、或7+16位、或7+64位
  • 0~126:扩展头部长度为0字节,后面全部为主体数据

  • 126:扩展头部长度为2字节,后面全部为主体数据

  • 127:扩展头部长度为8字节,后面全部为主体数据


主体数据:
  • Masking-key:mask为1时4个字节,用于解密后面的数据

  • Payload Data:密文数据


        客户端向服务端发送信息hello,未解密情况下conn.recv(8096)值为b'\x81\x85\xccA1\xdd\xa4$]\xb1\xa3',将第二个字节与127进行按位与运算,得到payload_len值为5小于126无Extended payload,第三字节到第六字节为Masking-key,后面的全部数据都是PayloadData

一张一弛之Websocket攻防总结
 
# 按位与操作得到Payload length长度
info = conn.recv(8096)payload_len = info[1] &127

# 根据不同payload_len长度得到mask和加密后的数据
# 主体数据中的前四字节为Masking-key,用于解码后面的消息

if payload_len ==126: extend_payload_len =info[2:4] mask = info[4:8] decoded =info[8:]elifpayload_len ==127: extend_payload_len =info[2:10] mask = info[10:14] decoded =info[14:]else: extend_payload_len =None mask = info[2:6] decoded =info[6:]

# 对PayloadData进行循环异或解密操作
bytes_list =bytearray()for i inrange(len(decoded)):   chunk = decoded[i] ^ mask[i %4]   bytes_list.append(chunk) body =str(bytes_list,encoding='utf-8')

04

处理即将发送的数据

       将上一步中的顺序颠倒构造数据包,其中需要注意的是从服务端向客户端发送数据时,不需要对数据进行掩码操作,mask为0

一张一弛之Websocket攻防总结

msg_bytes =bytes('hello',encoding="utf-8")token = b"\x81"#1000 0001

# 计算消息长度,写入到Payload len中
length =len(msg_bytes) #5if length <126:  token += struct.pack("B"length)elif length <=0xFFFF: token += struct.pack("!BH", 126, length)else: token += struct.pack("!BQ", 127, length)msg = token + msg_bytes # b'\x81\x05hello'conn.send(msg)

2.3 实例

单线程半交互式webshell

01

客户端


   
     
     
   
<!DOCTYPE html> <html> <head> <meta charset="utf-8"/> <title></title> </head> <body>
<div>发送:</div> <input type="text" id="msgContent"/> <input type="button" value="发送" onclick="CHAT.chat()"/>
<!-- <div>接收:</div> <divid="receiveMsg" style="background-color:gainsboro;"></div> !-->
<script type="application/javascript"> var msg =document.getElementById("msgContent"); window.CHAT= { socket:null, init:function() { if (window.WebSocket) { CHAT.socket=newWebSocket("ws://127.0.0.1:8080"); CHAT.socket.onopen=function() { console.log("success"); }, CHAT.socket.onclose=function() { console.log("close"); }, CHAT.socket.onerror=function() { console.log("err"); }, CHAT.socket.onmessage=function(e) { console.log("发送:\n"+ msg.value) console.log("接收:\n"+ e.data); // var receiveMsg =document.getElementById("receiveMsg"); // var html = receiveMsg.innerHTML; // receiveMsg.innerHTML = html + "<br/>" +e.data + "<br/>"; } } else { alert("buzhichi"); } }, chat:function() { CHAT.socket.send(msg.value); } };
CHAT.init(); </script> </body> </html>

02

服务端

import osimport socketimport base64import structimport hashlib

def get_headers(data): # 将http请求头数据转换成字典 header_dict = {} for i in data.decode('utf-8').split('\r\n'): if ': ' in i: k,v = i.split(': ', 1) header_dict[k] = v return header_dict
def get_data(info): # 处理收到的数据 payload_len = info[1] & 127 if payload_len == 126: extend_payload_len = info[2:4] mask = info[4:8] decoded = info[8:] elif payload_len == 127: extend_payload_len = info[2:10] mask = info[10:14] decoded = info[14:] else: extend_payload_len = None mask = info[2:6] decoded = info[6:]
bytes_list = bytearray() for i in range(len(decoded)): chunk = decoded[i] ^ mask[i % 4] bytes_list.append(chunk) body = str(bytes_list, encoding='utf-8') return body
def send_msg(conn, msg_bytes): # 处理即将发送的数据 token = b"\x81" length = len(msg_bytes) if length < 126: token += struct.pack("B", length) elif length <= 0xFFFF: token += struct.pack("!BH", 126, length) else: token += struct.pack("!BQ", 127, length)
msg = token + msg_bytes conn.send(msg) return True
def start(): # 创建socket对象,监听请求 sock = socket.socket() sock.setsockopt(socket.SOL_SOCKET,socket.SO_REUSEADDR,1) sock.bind(("127.0.0.1",8080)) sock.listen(5) conn,addr = sock.accept() data = conn.recv(8096)
# 拼接返回包 headers = get_headers(data) response_tpl = "HTTP/1.1 101 Switching Protocols\r\nUpgrade:websocket\r\nConnection: Upgrade\r\nSec-WebSocket-Accept: %s\r\n\r\n" magic_string = '258EAFA5-E914-47DA-95CA-C5AB0DC85B11' value = headers['Sec-WebSocket-Key'] + magic_string ac = base64.b64encode(hashlib.sha1(value.encode('utf-8')).digest()) # 发送返回包 response_str = response_tpl % (ac.decode('utf-8')) conn.send(bytes(response_str, encoding='utf-8'))
# 收发数据 while True: data = conn.recv(8096) data = get_data(data) print(data) # send_msg(conn,bytes(data,encoding="utf-8")) cmd = os.popen(data) send_msg(conn,bytes(cmd.read(),encoding="utf-8"))
if __name__ == "__main__":    start()

03

演示


一张一弛之Websocket攻防总结


漏洞


3.1 XSS


01

漏洞

      如果对输出的数据没有处理,直接发送给浏览器渲染,便可能存在xss注入

一张一弛之Websocket攻防总结
 

02

修复

      可以在服务端对数据中敏感字符进行转义,如:尖角符号html实体编码

   
     
     
   
def html_trans(data): data = data.replace('<','&lt;') data = data.replace('>','&gt;') return data

      浏览器渲染解析HTML生成DOM树时无法识别&lt;等字符,后续渲染过程中识别到实体解码进行解码,最终将原生字符展示在用户界面

一张一弛之Websocket攻防总结
 

3.2 DOS


01

漏洞

      websocket建立的是持久连接,客户端或服务端其中一发提出关闭连接的请求,连接才会关闭,存在拒绝服务漏洞

  • 可以向服务端发起大量申请建立websocket连接的请求,建立持久连接,消耗服务器资源

  • 可以发送一个单一的庞大的数据帧(如:2^16),或者发送一个长流的分片消息的小帧,消耗服务器资源


02

修复

  • 设置单IP可建立连接的最大连接数

  • 限制帧大小和多个帧重组后的总消息大小


3.3 AUTH

01

漏洞

      上述代码未加入身份认证功能,服务端响应任何客户端的请求,如将其投入至生产环境中,攻击者只要知道ws地址便可以发起命令执行攻击操控整个服务器

02

修复

      服务端添加身份认证机制,如:token随机数验证、cookie身份验证
# 加入cookie验证
   
     
     
   
if headers['Cookies'] == xxx: ... conn.send(bytes(response_str, encoding='utf-8'))

        身份验证失败,已无法建立连接
一张一弛之Websocket攻防总结

3.4 CSWSH


01

漏洞

      漏洞原理类似csrf,被攻击者失误点击了漏洞exp链接,以当前用户身份向服务器发送ws请求

01

修复

      服务端添加请求来源检测代码,如:origin验证、token随机数验证
# 加入Token验证
   
     
     
   
if headers['Token'] == xxx: ... conn.send(bytes(response_str, encoding='utf-8'))

        来源验证失败,已无法建立连接
一张一弛之Websocket攻防总结

参考

  • https://www.cnblogs.com/ssyfj/p/9245150.html

  • https://www.cnblogs.com/lichmama/p/3931212.html

  • https://security.tencent.com/index.php/blog/msg/119

  • https://www.cnblogs.com/songwenjie/p/8575579.html

  • http://www.ruanyifeng.com/blog/2017/05/websocket.html

  • https://developer.ibm.com/zh/articles/j-lo-websocket-cross-site/





点击阅读原文
与瑞数技术专家深度交流!