记录一下最近遇到的一个XSS以及尝试升级XSS漏洞的过程。
怎么找到的
在hackerone项目里找到一个目标,*.target.com。
爆破以及侦察域名,轻车熟路,建议用shell命令集成好
1 | rm subs |
这样我们就得到了一些域名,可以通过HTTPX查看域名状态,大概心里有个数。
1 | cat subhttpx | httpx -td -ip -sc -t 100 |
习惯先用waybackurls,看一下历史数据
1 | echo target.com | waybackurls > back |
如果喜欢在浏览器上看,可以用
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
- 直接修改targetUrl的值变成,javascript:alert(),发现Waf被拦了。
- 修改成随机数,看前端有没有函数处理TargetUrl参数,打上断点,没有发现有函数处理,没有发现使用Dom直接跳转。
看看反射
- 修改成随机数之后,发包,发现返回包变成429 Too Many Request,不对劲。
- 把之前的参数留着,在原有参数上,加上随机数,返回包正常,在返回包中发现我们的随机数在javscript的location.href的变量里,用双引号包裹。
- 抱着试一试的心态,使用双引号尝试闭合变量,发现成功了,意味着我们可以直接执行javscript,一个标准的反射XSS。
绕 Akamai WAF
在这个场景下,我们有很多种执行JavaScript代码的选择。
- 我闭合这个变量,直接执行JavaScript代码,并闭合后面的双引号。
- 在这个变量下,直接使用表达式。我们可以使用运算符(+、-、*、/)等等,对这个变量的值进行运算,运算的同时可以执行我们的表达式。
我选择闭合变量,直接执行javascript代码。那么我们注入的代码应该类似是
1 | ";alert();var a=" |
不出所料,被Akamai拦截。
凭我多年对抗Waf的经验,以及编写雷池Waf检测逻辑的经验,prompt和comfirm一定是用不了的。
我们需要找到一些新颖的表达式,同时符合JavaScript语法,又同时能过Waf。
靠记忆是很难一下子绕过的,看看之前存下来的常规payload
1 | confirm() |
又全军覆没了。
绕过Waf的方式有两种,一种是通过对抗检测XSS的检测逻辑,不管是语义还是正则。一种是从它本身的协议解析入手。
拓展一下我对协议解析绕Waf的理解
我们知道,一个完整的请求到达检测引擎之后,检测引擎想检测必然要把参数的Key和Value分开来,Get参数是通过 & 符合进行分隔开的。假如我们能构造一个payload,使得协议解析时对Get参数的分离产生错误,那么检测Payload的分离它也就是错误的。
来看一个例子,它曾经能绕过Akamai的Waf
1 | <body x='&'onload="(alert)('citrix akamai bypass')"> |
在协议解析绕过Waf的空间大很多,只要你有充足的想象力。
对抗XSS检测逻辑
回到原点,此payload对于我们来说用处不大,因为我们无法闭合标签并生成新的标签。
我打算从self对象入手,self返回的对象跟window对象是一模一样的,window对象的常用方法和函数都可以用self代替window,因此self可以直接调用eval函数,当然eval函数比alert、prompt弹窗函数更危险。
1 | ";self["eval"](alert());var a=" |
截取了几个经典的有思路的例子,都被拦截了,期间也换过很多Dom对象,都没有用。
该去看一点新鲜的Javascript表达式了。
[https://developer.mozilla.org/zh-CN/docs/Web/JavaScript/Reference]
1 | 逻辑空赋值运算符(x ??= y)仅在 x 是空值(null 或 undefined)时对其赋值。 |
最终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 | var xhr = new XMLHttpRequest(); |
我把这段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的错误,百思不得其解,后来才设置为同步。