Skip to content
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

Add at rest encryption to Meterpreter payloads #679

Merged
merged 8 commits into from
Oct 13, 2023
5 changes: 4 additions & 1 deletion gem/Rakefile
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
require "bundler/gem_tasks"
require 'openssl'
require 'metasploit-payloads/crypto'

c_source = "../c/meterpreter/"
java_source = "../java"
Expand Down Expand Up @@ -52,7 +53,9 @@ def copy_files(cnf, meterpreter_dest)
Dir.glob("#{f}/*.#{ext}").each do |bin|
target = File.join(meterpreter_dest, File.basename(bin))
print("Copying: #{bin} -> #{target}\n")
FileUtils.cp(bin, target)
contents = ::File.binread(::File.expand_path(bin))
encrypted_contents = ::MetasploitPayloads::Crypto.encrypt(plaintext: contents)
::File.binwrite(::File.expand_path(target), encrypted_contents)
end
end
end
Expand Down
24 changes: 18 additions & 6 deletions gem/lib/metasploit-payloads.rb
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
require 'openssl' unless defined? OpenSSL::Digest
require 'metasploit-payloads/version' unless defined? MetasploitPayloads::VERSION
require 'metasploit-payloads/error' unless defined? MetasploitPayloads::Error
require 'metasploit-payloads/crypto' unless defined? MetasploitPayloads::Crypto

#
# This module dispenses Metasploit payload binary files
Expand Down Expand Up @@ -43,8 +44,9 @@ def self.manifest_errors
manifest_contents.each_line do |line|
filename, hash_type, hash = line.chomp.split(':')
begin
filename = filename.sub('./data/', '')
# self.path prepends the gem data directory, which is already present in the manifest file.
out_path = self.path(filename.sub('./data/', ''))
out_path = self.path(filename)
# self.path can return a path to the gem data, or user's local data.
bundled_file = out_path.start_with?(data_directory)
if bundled_file
Expand Down Expand Up @@ -137,15 +139,25 @@ def self.path(*path_parts)

#
# Get the contents of any file packaged in this gem by local path and name.
# If the file is encrypted using ChaCha20, automatically decrypt it and return the file contents.
#
def self.read(*path_parts)
file_path = path(path_parts)
if file_path.nil?
full_path = ::File.join(path_parts)
raise ::MetasploitPayloads::NotFoundError, full_path, caller
file_path = self.path(path_parts)

begin
file_contents = ::File.binread(file_path)
rescue ::Errno::ENOENT => _e
raise ::MetasploitPayloads::NotFoundError, file_path, caller
rescue ::Errno::EACCES => _e
raise ::MetasploitPayloads::NotReadableError, file_path, caller
rescue ::StandardError => e
raise e
end

::File.binread(file_path)
encrypted_file = file_contents.start_with?(Crypto::ENCRYPTED_PAYLOAD_HEADER)
return file_contents unless encrypted_file

Crypto.decrypt(ciphertext: file_contents)
end

#
Expand Down
44 changes: 44 additions & 0 deletions gem/lib/metasploit-payloads/crypto.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
require 'openssl'

module MetasploitPayloads
module Crypto
CIPHER_NAME = 'chacha20'.b.freeze
adfoster-r7 marked this conversation as resolved.
Show resolved Hide resolved
IV = 'EncryptedPayload'.b.freeze # 16 bytes
KEY = 'Rapid7MetasploitEncryptedPayload'.b.freeze # 32 bytes
ENCRYPTED_PAYLOAD_HEADER = 'encrypted_payload_chacha20_v1'.ljust(64, '_').b.freeze
adfoster-r7 marked this conversation as resolved.
Show resolved Hide resolved

def self.encrypt(plaintext: '')
raise ::ArgumentError, 'Unable to encrypt plaintext: ' << plaintext, caller unless plaintext.to_s

cipher = ::OpenSSL::Cipher.new(CIPHER_NAME)

cipher.encrypt
cipher.iv = IV
cipher.key = KEY

output = ENCRYPTED_PAYLOAD_HEADER.dup
output << cipher.update(plaintext)
output << cipher.final

output
end

def self.decrypt(ciphertext: '')
raise ::ArgumentError, 'Unable to decrypt ciphertext: ' << ciphertext, caller unless ciphertext.to_s

cipher = ::OpenSSL::Cipher.new(CIPHER_NAME)

cipher.decrypt
cipher.iv = IV
cipher.key = KEY

# Remove encrypted header if present
ciphertext = ciphertext.sub(ENCRYPTED_PAYLOAD_HEADER, '')

output = cipher.update(ciphertext)
output << cipher.final

output
end
end
end
22 changes: 22 additions & 0 deletions gem/spec/metasploit_payloads/crypto_spec.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
require 'spec_helper'
require 'metasploit-payloads'

RSpec.describe ::MetasploitPayloads::Crypto do
describe '#encrypt' do
let(:encrypted_header) { "encrypted_payload_chacha20_v1".ljust(64, '_').b }
let(:plaintext) { "Hello World!".b }
let(:ciphertext) { encrypted_header + "\\c\xB6N\x95\xE58\x8D\xDF\xBF4c".b }

it 'can encrypt plaintext' do
expect(described_class.encrypt(plaintext: plaintext)).to eq ciphertext
end

it 'can decrypt ciphertext' do
expect(described_class.decrypt(ciphertext: ciphertext)).to eq plaintext
end

it 'is idempotent' do
expect(described_class.decrypt(ciphertext: described_class.encrypt(plaintext: plaintext))).to eq plaintext
end
end
end
32 changes: 32 additions & 0 deletions gem/spec/metasploit_payloads/metasploit_payloads_spec.rb
Original file line number Diff line number Diff line change
Expand Up @@ -226,6 +226,8 @@
bundled_file_path = ::MetasploitPayloads.expand(::MetasploitPayloads.data_directory, sample_file[:name])
error = ::MetasploitPayloads::HashMismatchError.new(bundled_file_path)
allow(::MetasploitPayloads).to receive(:path).with(sample_file[:name]).and_return(bundled_file_path)
# Second call to self.read takes in the splat operator
allow(::MetasploitPayloads).to receive(:path).with([sample_file[:name]]).and_return(bundled_file_path)
allow(::File).to receive(:binread).with(bundled_file_path).and_return('sample_mismatched_contents')

expect(subject.manifest_errors).to include({ path: bundled_file_path, error: error })
Expand All @@ -246,4 +248,34 @@
end
end
end

describe '#read' do
let(:encrypted_header) { 'encrypted_payload_chacha20_v1' }
let(:raw_file) { { name: 'meterpreter.py', contents: 'sample_file_contents' } }
# ChaCha20 encrypted contents
let(:encrypted_contents) { "gg\xB7R\x96\xA00\x84\xC4\xBF5\x1D\xDBG6J\n\x86\x06\xF1" }
let(:encrypted_file) { { name: raw_file[:name], contents: encrypted_header + encrypted_contents } }

before :each do
allow(::MetasploitPayloads).to receive(:path).and_call_original
allow(::MetasploitPayloads).to receive(:path).with([encrypted_file[:name]]).and_return(encrypted_file[:name])
allow(::MetasploitPayloads).to receive(:path).with([raw_file[:name]]).and_return(raw_file[:name])

allow(::File).to receive(:binread).and_call_original
allow(::File).to receive(:binread).with(encrypted_file[:name]).and_return(encrypted_file[:contents])
allow(::File).to receive(:binread).with(raw_file[:name]).and_return(raw_file[:contents])
end

context 'an encrypted file' do
it 'returns plain-text file contents' do
expect(subject.read(encrypted_file[:name])).to eq(raw_file[:contents])
end
end

context 'a plain-text file' do
it 'returns plain-text file contents' do
expect(subject.read(raw_file[:name])).to eq(raw_file[:contents])
end
end
end
cgranleese-r7 marked this conversation as resolved.
Show resolved Hide resolved
end