这几天一直有事,导致之前的比赛的WP也没看,然后靶场这方面的学习也没推进下去,终于找了个时间的空挡做了道关于Ruby的模板注入。为了这道题我甚至抽出了我本就不多的时间去学了学Ruby的语法,话说学完Ruby之后,我的感触是他与Python等脚本语言很像,但是它的语法中字里行间都透露着Java等语言的关系。关于这次的SSTI题目,与其他的SSTI有着很大的不同。
老样子,我们先看题目
题目的意思很简单就是我们要通过足够的jinkela来购买flag。但是我们发现我们的初始金额数是不够的,那怎么办呢?我们可以通过点击work选项来增加jinkela的数量,但是通过work所获得的jinkela数量有限。可是那又怎么样呢?我们只要点的多,我们总会达到那个数字的(bushi。
咳咳,咱们言归正传,我们扫描一下目录发现有robots.txt文件,我们访问一下看看。
我们直接访问一下/filebak目录,里面是这个网站的源码。
require 'sinatra'
require 'sinatra/cookies'
require 'sinatra/json'
require 'jwt'
require 'securerandom'
require 'erb'
set :public_folder, File.dirname(__FILE__) + '/static'
FLAGPRICE = 1000000000000000000000000000
ENV["SECRET"] = SecureRandom.hex(64)
configure do
enable :logging
file = File.new(File.dirname(__FILE__) + '/../log/http.log',"a+")
file.sync = true
use Rack::CommonLogger, file
end
get "/" do
redirect '/shop', 302
end
get "/filebak" do
content_type :text
erb IO.binread __FILE__
end
get "/api/auth" do
payload = { uid: SecureRandom.uuid , jkl: 20}
auth = JWT.encode payload,ENV["SECRET"] , 'HS256'
cookies[:auth] = auth
end
get "/api/info" do
islogin
auth = JWT.decode cookies[:auth],ENV["SECRET"] , true, { algorithm: 'HS256' }
json({uid: auth[0]["uid"],jkl: auth[0]["jkl"]})
end
get "/shop" do
erb :shop
end
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("").result
end
end
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
def islogin
if cookies[:auth].nil? then
redirect to('/shop')
end
end
我们发现这个源码中有JWT,我们抓一个包试试看。
我们拿着这个JWT去解密。
我们发现这个网站的jinkela数量是通过JWT进行传递的,所以我们只要能够得到这道题的加密密钥,从而伪造JWT,就行了。那么,我们该如何拿到密钥呢?从上面的整个代码中着重看如下代码。
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("").result
end
end
这里涉及到了一些关于Ruby ERB模板注入的知识,具体的就不细说了,原理都差不多,只是语法的区别。
ERB模板注入链接
我们要通过<%=%>进行模板注入,我们想要得到SECRET但是在<%=%>中我们不能直接写SECRET,因为params[:name][0,7]
的存在导致我们只能在模板内写入2个字符。但是幸运的是,Ruby为我们提供了预定义字符。
$’ 最后一次模式匹配中匹配部分之后的字符串
让我们看看运行到这句话之前的最后一个模式匹配在哪里?
unless params[:SECRET].nil?
if ENV["SECRET"].match("#{params[:SECRET].match(/[0-9a-z]+/)}")
puts ENV["FLAG"]
end
就是在匹配SECRET,这个预定义字符的作用是将匹配之后的字符进行返回。什么意思呢?我来举个例子
hello world //我设置匹配字符为e
llo world//这就是返回值
我们要想得到完整的SECRET,那就必须传进去一个空的SECRET,让最后的返回值是完整的。
所以我们如此构造payload
?name=<%=$'%>&do=<%=$' is working%>&SECRET=
结果如下
我们拿着这个密钥去伪造JWT
我们把这个伪造的JWT发过去。
我们看到服务端又给我返回了一个新的JWT,我们这次把他解密看看。
返回了flag.
虽然这段忙的时间还没有过去,但是总有时间是可以腾出来的。之后就应该开始SSRF的学习了,SSTI也要告一段落了,应该和SQL一样把他们放在比赛中继续磨练了。总之当下的问题是先熬过这段艰苦的时光。