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
66 changes: 66 additions & 0 deletions gem/lib/metasploit-payloads/crypto.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,66 @@
require 'openssl'

module MetasploitPayloads
module Crypto
CIPHERS = {
chacha20: {
name: 'chacha20'.b,
version: 1,
iv: {
value: 'EncryptedPayload'.b, # 16 bytes
sjanusz-r7 marked this conversation as resolved.
Show resolved Hide resolved
version: 1
},
key: {
value: 'Rapid7MetasploitEncryptedPayload'.b, # 32 bytes
version: 1
}
}
}.freeze
CURRENT_CIPHER = CIPHERS[:chacha20]
CIPHER_VERSION = CURRENT_CIPHER[:version]
KEY_VERSION = CURRENT_CIPHER[:key][:version]
IV_VERSION = CURRENT_CIPHER[:iv][:version]
# Binary String, unsigned char, unsigned char, unsigned char
ENCRYPTED_PAYLOAD_HEADER = ['msf', CIPHER_VERSION, IV_VERSION, KEY_VERSION].pack('A*CCC')

private_constant :CIPHERS
private_constant :CURRENT_CIPHER
private_constant :CIPHER_VERSION
private_constant :KEY_VERSION
private_constant :IV_VERSION

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

cipher = ::OpenSSL::Cipher.new(CURRENT_CIPHER[:name])

cipher.encrypt
cipher.iv = CURRENT_CIPHER[:iv][:value]
cipher.key = CURRENT_CIPHER[:key][:value]

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(CURRENT_CIPHER[:name])

cipher.decrypt
cipher.iv = CURRENT_CIPHER[:iv][:value]
cipher.key = CURRENT_CIPHER[:key][:value]

# 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) { ::MetasploitPayloads::Crypto::ENCRYPTED_PAYLOAD_HEADER }
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
30 changes: 30 additions & 0 deletions gem/spec/metasploit_payloads/metasploit_payloads_spec.rb
Original file line number Diff line number Diff line change
Expand Up @@ -246,4 +246,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