BUU刷题记录-[SCTF2019]Flag Shop

解题过程

 ​ ​ ​ ​ ​ 看到题目关键字shop,怀疑需要抓包修改money进行购买,而金额一般通过加密隐藏在cookie字段中,接下来就着重观察cookie字段。

 ​ ​ ​ ​ ​ 抓包得到cookie字段,

auth=eyJhbGciOiJIUzI1NiJ9.eyJ1aWQiOiJmNGE3OWEyMy05YjA4LTRhZDUtODgyMS05YzEzY2M1MDdmYWIiLCJqa2wiOjMwfQ.ex-ThOTfqKGa3TqShqqkU-aF2VhY6i9KJaddUao7mW4

很可疑,看着像JWT,于是拿到解密网站看看。

接下来就直接jwtcracker爆破,半个小时过去了。。。除了风扇狂笑(,我一无所获,无计可施的我只能直接跟着大佬wp走,没想到第一步就懵逼了。

???为什么要robots.txt,又是脑洞吗(还是我太菜了...),想了想参加过的比赛都规定不需要用扫描器,也就没往这方面想,下次一定。

访问robots.txt得到一个文件地址/filebak,访问得到源码:

 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
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("<script>alert('#{params[:name][0,7]} working successfully!')</script>").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

ruby源码(这不是欺负人吗),不会,果断放弃审计,直接下一步--erb模板注入。

知道了模板注入之后我们可以尝试着审计一下源码,结合以前做题的经验,ENV["SECRET"]应该是我们注入的目标,#{params[:SECRET].match(/[0-9a-z]+/)}#{params[:name][0,7]}可能是我们模板注入的利用点,因为这些都是我们可控的字符,百度到erb常用模板注入payload<%=攻击字段%>,但是具体利用还是不会。

依照wp所说,在/work路由中,[0,7]应该是我们通过GET传递的name字段的长度,且ENV["SECRET"].match("#{params[:SECRET].match(/[0-9a-z]+/)}")的作用是对密钥正则。

于是,新的姿势出现了--用ruby的预定义字符$对匹配的字符串读取secret(ruby的特性:预定义变量), 构造payload:

/work?SECRET=&name=%3c%25%3d%24%27%25%3e&do=%3c%25%3d%24%27%25%3e%20is%20working

分析一下这个payload:

  1. ENV["SECRET"].match("#{params[:SECRET].match(/[0-9a-z]+/)}")正则判断上传的SECRET字段不能包含pattern中的内容,因此这里选择了传递空字符。
  2. params[:do] == "#{params[:name][0,7]} is working"判断上传的do字段和name字段是否相同,而我们构造 <%=$'%>,根据$'-最后一次成功匹配右侧的字符串的作用,我们使name和do成功匹配,上传时要urlencode。

获得密钥secret:

90b980318681b75c0ace1d7916d53d14ba419244b27d1f8b68ea3482889f9d9f8e013c7a021b4f119998f1e4d2b194062ea963e17606a52f0eb3bcc3bc77d655

构造cookie:

提交回显新cookie,JWT解码得flag。

总结

  1. 没事干就扫目录(×
  2. erb模板注入payload<%=字段%>
  3. ruby的特性:预定义变量