From 62c599b2ff691d9cb8de433b3b894281d961985d Mon Sep 17 00:00:00 2001 From: sjanusz-r7 Date: Tue, 24 Oct 2023 10:07:07 +0100 Subject: [PATCH 1/5] Swap ChaCha20 to AES-256-CBC for at-rest encryption --- gem/lib/metasploit-payloads/crypto.rb | 14 +++++++++++++- gem/spec/metasploit_payloads/crypto_spec.rb | 2 +- 2 files changed, 14 insertions(+), 2 deletions(-) diff --git a/gem/lib/metasploit-payloads/crypto.rb b/gem/lib/metasploit-payloads/crypto.rb index 747e3ced1..8b27ce2c9 100644 --- a/gem/lib/metasploit-payloads/crypto.rb +++ b/gem/lib/metasploit-payloads/crypto.rb @@ -14,9 +14,21 @@ module Crypto value: "\x28\x39\x97\x4c\x95\x11\x9d\x42\x6c\x8b\xff\x43\x3e\x5d\x3c\x33\x1b\x95\xd3\xea\xeb\xc9\xae\x71\x0a\x36\xe7\x98\x3d\x9d\x09\x52".b, # 32 bytes version: 1 } + }, + aes_256_cbc: { + name: 'aes-256-cbc'.b, + version: 2, + iv: { + value: "\x3c\x09\x85\x95\x19\x09\x10\xff\x76\xf0\x48\xf7\x21\x1a\x5c\x59".b, # 16 bytes + version: 1 + }, + key: { + value: "\x01\x93\x90\xfb\x84\xcd\x70\x16\x90\x1d\xc6\xf4\xf2\xfd\xcf\x59\xc4\x9c\x26\x35\x29\x67\x8c\x2d\x17\xb9\x35\xcb\x7d\xb0\x88\x7a".b, # 32 bytes + version: 1 + } } }.freeze - CURRENT_CIPHER = CIPHERS[:chacha20] + CURRENT_CIPHER = CIPHERS[:aes_256_cbc] CIPHER_VERSION = CURRENT_CIPHER[:version] KEY_VERSION = CURRENT_CIPHER[:key][:version] IV_VERSION = CURRENT_CIPHER[:iv][:version] diff --git a/gem/spec/metasploit_payloads/crypto_spec.rb b/gem/spec/metasploit_payloads/crypto_spec.rb index eb41d19e0..d7595d2ea 100644 --- a/gem/spec/metasploit_payloads/crypto_spec.rb +++ b/gem/spec/metasploit_payloads/crypto_spec.rb @@ -5,7 +5,7 @@ describe '#encrypt' do let(:encrypted_header) { ::MetasploitPayloads::Crypto::ENCRYPTED_PAYLOAD_HEADER } let(:plaintext) { "Hello World!".b } - let(:ciphertext) { encrypted_header + "\x89:^r\xC1\xC9\xD9\xA1\xDC\xEB\xBFm".b } + let(:ciphertext) { encrypted_header + "F=\xF9\xCB\xF6\xA1\xE4h\x89\x96DD\xC0+\x04\xF1".b } it 'can encrypt plaintext' do expect(described_class.encrypt(plaintext: plaintext)).to eq ciphertext From 1a17ffc65b9e455ad9ba61bafe78ad1710a4617f Mon Sep 17 00:00:00 2001 From: sjanusz-r7 Date: Tue, 24 Oct 2023 11:52:40 +0100 Subject: [PATCH 2/5] Allow for backwards-compatible decryption --- gem/lib/metasploit-payloads/crypto.rb | 63 ++++++++++++++++----------- 1 file changed, 37 insertions(+), 26 deletions(-) diff --git a/gem/lib/metasploit-payloads/crypto.rb b/gem/lib/metasploit-payloads/crypto.rb index 8b27ce2c9..1e2503757 100644 --- a/gem/lib/metasploit-payloads/crypto.rb +++ b/gem/lib/metasploit-payloads/crypto.rb @@ -3,37 +3,42 @@ module MetasploitPayloads module Crypto CIPHERS = { - chacha20: { + 1 => { name: 'chacha20'.b, - version: 1, - iv: { - value: "\x52\x25\xd7\xab\x52\x8f\x3f\xf8\x94\x97\x08\x42\x33\xb9\xd3\xb6".b, # 16 bytes - version: 1 + version: { iv: 1, key: 1 }, + ivs: { + 1 => { + value: "\x52\x25\xd7\xab\x52\x8f\x3f\xf8\x94\x97\x08\x42\x33\xb9\xd3\xb6".b # 16 bytes + } }, - key: { - value: "\x28\x39\x97\x4c\x95\x11\x9d\x42\x6c\x8b\xff\x43\x3e\x5d\x3c\x33\x1b\x95\xd3\xea\xeb\xc9\xae\x71\x0a\x36\xe7\x98\x3d\x9d\x09\x52".b, # 32 bytes - version: 1 + keys: { + 1 => { + value: "\x28\x39\x97\x4c\x95\x11\x9d\x42\x6c\x8b\xff\x43\x3e\x5d\x3c\x33\x1b\x95\xd3\xea\xeb\xc9\xae\x71\x0a\x36\xe7\x98\x3d\x9d\x09\x52".b, # 32 bytes + } } }, - aes_256_cbc: { + 2 => { name: 'aes-256-cbc'.b, - version: 2, - iv: { - value: "\x3c\x09\x85\x95\x19\x09\x10\xff\x76\xf0\x48\xf7\x21\x1a\x5c\x59".b, # 16 bytes - version: 1 + version: { iv: 1, key: 1 }, + ivs: { + 1 => { + value: "\x3c\x09\x85\x95\x19\x09\x10\xff\x76\xf0\x48\xf7\x21\x1a\x5c\x59".b, # 16 bytes + } }, - key: { - value: "\x01\x93\x90\xfb\x84\xcd\x70\x16\x90\x1d\xc6\xf4\xf2\xfd\xcf\x59\xc4\x9c\x26\x35\x29\x67\x8c\x2d\x17\xb9\x35\xcb\x7d\xb0\x88\x7a".b, # 32 bytes - version: 1 + keys: { + 1 => { + value: "\x01\x93\x90\xfb\x84\xcd\x70\x16\x90\x1d\xc6\xf4\xf2\xfd\xcf\x59\xc4\x9c\x26\x35\x29\x67\x8c\x2d\x17\xb9\x35\xcb\x7d\xb0\x88\x7a".b, # 32 bytes + } } } }.freeze - CURRENT_CIPHER = CIPHERS[:aes_256_cbc] - CIPHER_VERSION = CURRENT_CIPHER[:version] - KEY_VERSION = CURRENT_CIPHER[:key][:version] - IV_VERSION = CURRENT_CIPHER[:iv][:version] + CIPHER_VERSION = 2 + CURRENT_CIPHER = CIPHERS[CIPHER_VERSION] + KEY_VERSION = CURRENT_CIPHER[:version][:key] + IV_VERSION = CURRENT_CIPHER[:version][:iv] + # Binary String, unsigned char, unsigned char, unsigned char - ENCRYPTED_PAYLOAD_HEADER = ['msf', CIPHER_VERSION, IV_VERSION, KEY_VERSION].pack('A*CCC') + ENCRYPTED_PAYLOAD_HEADER = ['msf', CIPHER_VERSION, IV_VERSION, KEY_VERSION].pack('A*CCC').freeze private_constant :CIPHERS private_constant :CURRENT_CIPHER @@ -47,8 +52,8 @@ def self.encrypt(plaintext: '') cipher = ::OpenSSL::Cipher.new(CURRENT_CIPHER[:name]) cipher.encrypt - cipher.iv = CURRENT_CIPHER[:iv][:value] - cipher.key = CURRENT_CIPHER[:key][:value] + cipher.iv = CURRENT_CIPHER[:ivs][IV_VERSION][:value] + cipher.key = CURRENT_CIPHER[:keys][KEY_VERSION][:value] output = ENCRYPTED_PAYLOAD_HEADER.dup output << cipher.update(plaintext) @@ -60,11 +65,17 @@ def self.encrypt(plaintext: '') def self.decrypt(ciphertext: '') raise ::ArgumentError, 'Unable to decrypt ciphertext: ' << ciphertext, caller unless ciphertext.to_s - cipher = ::OpenSSL::Cipher.new(CURRENT_CIPHER[:name]) + # Use the correct algorithm based on the version in the header + _msf_header, cipher_version, iv_version, key_version = ciphertext.unpack('A3CCC') + + current_cipher = CIPHERS[cipher_version] + cipher = ::OpenSSL::Cipher.new(current_cipher[:name]) + iv = current_cipher[:ivs][iv_version][:value] + key = current_cipher[:keys][key_version][:value] cipher.decrypt - cipher.iv = CURRENT_CIPHER[:iv][:value] - cipher.key = CURRENT_CIPHER[:key][:value] + cipher.iv = iv + cipher.key = key # Remove encrypted header if present ciphertext = ciphertext.sub(ENCRYPTED_PAYLOAD_HEADER, '') From aaf3d1130e51bc41b4ec90bbc594fc6521aa1809 Mon Sep 17 00:00:00 2001 From: sjanusz-r7 Date: Tue, 24 Oct 2023 11:53:07 +0100 Subject: [PATCH 3/5] Don't run rspec tests twice --- gem/spec/spec_helper.rb | 2 -- 1 file changed, 2 deletions(-) diff --git a/gem/spec/spec_helper.rb b/gem/spec/spec_helper.rb index d12ba0953..b429c1548 100644 --- a/gem/spec/spec_helper.rb +++ b/gem/spec/spec_helper.rb @@ -1,7 +1,5 @@ # frozen_string_literal: true -require 'metasploit_payloads/metasploit_payloads_spec' - # This file was generated by the `rspec --init` command. Conventionally, all # specs live under a `spec` directory, which RSpec adds to the `$LOAD_PATH`. # The generated `.rspec` file contains `--require spec_helper` which will cause From 1bc7e3e3ae9bf2551fc371807ab8d2500b5f1c5d Mon Sep 17 00:00:00 2001 From: sjanusz-r7 Date: Tue, 24 Oct 2023 14:25:49 +0100 Subject: [PATCH 4/5] Use the correct encrypted payload header in tests --- gem/spec/metasploit_payloads/metasploit_payloads_spec.rb | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/gem/spec/metasploit_payloads/metasploit_payloads_spec.rb b/gem/spec/metasploit_payloads/metasploit_payloads_spec.rb index caab4584a..9769a7718 100644 --- a/gem/spec/metasploit_payloads/metasploit_payloads_spec.rb +++ b/gem/spec/metasploit_payloads/metasploit_payloads_spec.rb @@ -248,10 +248,10 @@ end describe '#read' do - let(:encrypted_header) { 'encrypted_payload_chacha20_v1' } + let(:encrypted_header) { "msf\x02\x01\x01" } 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" } + # AES-256-CBC encrypted contents + let(:encrypted_contents) { "\xEA\x00q\xEB\a\xCA\xD2\xD3\xE2',N\x86\x1C\f?\xBE\xC4\x8AJRks\xAD\xD6\xDF\xA3.\xCD\xA7\x84\xD2".b } let(:encrypted_file) { { name: raw_file[:name], contents: encrypted_header + encrypted_contents } } before :each do From cf1b82f07ec0eee305ae81a5f613b83a9a027264 Mon Sep 17 00:00:00 2001 From: sjanusz-r7 Date: Tue, 24 Oct 2023 14:26:35 +0100 Subject: [PATCH 5/5] Test AES-256-CBC and ChaCha20 crypto --- gem/lib/metasploit-payloads.rb | 3 -- gem/lib/metasploit-payloads/crypto.rb | 9 +++-- gem/spec/metasploit_payloads/crypto_spec.rb | 44 ++++++++++++++++----- 3 files changed, 41 insertions(+), 15 deletions(-) diff --git a/gem/lib/metasploit-payloads.rb b/gem/lib/metasploit-payloads.rb index 18b8f3723..48a9c9d2f 100644 --- a/gem/lib/metasploit-payloads.rb +++ b/gem/lib/metasploit-payloads.rb @@ -154,9 +154,6 @@ def self.read(*path_parts) raise e end - encrypted_file = file_contents.start_with?(Crypto::ENCRYPTED_PAYLOAD_HEADER) - return file_contents unless encrypted_file - Crypto.decrypt(ciphertext: file_contents) end diff --git a/gem/lib/metasploit-payloads/crypto.rb b/gem/lib/metasploit-payloads/crypto.rb index 1e2503757..dbdbe276c 100644 --- a/gem/lib/metasploit-payloads/crypto.rb +++ b/gem/lib/metasploit-payloads/crypto.rb @@ -65,8 +65,10 @@ def self.encrypt(plaintext: '') def self.decrypt(ciphertext: '') raise ::ArgumentError, 'Unable to decrypt ciphertext: ' << ciphertext, caller unless ciphertext.to_s + return ciphertext unless ciphertext.start_with?('msf'.b) + # Use the correct algorithm based on the version in the header - _msf_header, cipher_version, iv_version, key_version = ciphertext.unpack('A3CCC') + msf_header, cipher_version, iv_version, key_version = ciphertext.unpack('A3CCC') current_cipher = CIPHERS[cipher_version] cipher = ::OpenSSL::Cipher.new(current_cipher[:name]) @@ -77,8 +79,9 @@ def self.decrypt(ciphertext: '') cipher.iv = iv cipher.key = key - # Remove encrypted header if present - ciphertext = ciphertext.sub(ENCRYPTED_PAYLOAD_HEADER, '') + header = [msf_header, cipher_version, iv_version, key_version].pack('A*CCC').b + # Remove encrypted header + ciphertext = ciphertext.sub(header, '') output = cipher.update(ciphertext) output << cipher.final diff --git a/gem/spec/metasploit_payloads/crypto_spec.rb b/gem/spec/metasploit_payloads/crypto_spec.rb index d7595d2ea..130d7eb0d 100644 --- a/gem/spec/metasploit_payloads/crypto_spec.rb +++ b/gem/spec/metasploit_payloads/crypto_spec.rb @@ -1,22 +1,48 @@ -require 'spec_helper' require 'metasploit-payloads' RSpec.describe ::MetasploitPayloads::Crypto do + let(:plaintext) { "Hello World!".b } + describe '#encrypt' do - let(:encrypted_header) { ::MetasploitPayloads::Crypto::ENCRYPTED_PAYLOAD_HEADER } - let(:plaintext) { "Hello World!".b } + let(:encrypted_header) { "msf\x02\x01\x01".b } let(:ciphertext) { encrypted_header + "F=\xF9\xCB\xF6\xA1\xE4h\x89\x96DD\xC0+\x04\xF1".b } - it 'can encrypt plaintext' do + it 'encrypts using aes-256-cbc' do expect(described_class.encrypt(plaintext: plaintext)).to eq ciphertext end + end - it 'can decrypt ciphertext' do - expect(described_class.decrypt(ciphertext: ciphertext)).to eq plaintext - end + describe '#decrypt' do + context 'when the ciphertext is' do + context 'encrypted with chacha20' do + let(:encrypted_header) { "msf\x01\x01\x01".b } + let(:ciphertext) { encrypted_header + "\x89:^r\xC1\xC9\xD9\xA1\xDC\xEB\xBFm".b } + + it 'returns plaintext' do + expect(described_class.decrypt(ciphertext: ciphertext)).to eq plaintext + end + end + + context 'encrypted with aes-256-cbc' do + let(:encrypted_header) { "msf\x02\x01\x01".b } + let(:ciphertext) { encrypted_header + "F=\xF9\xCB\xF6\xA1\xE4h\x89\x96DD\xC0+\x04\xF1".b } - it 'is idempotent' do - expect(described_class.decrypt(ciphertext: described_class.encrypt(plaintext: plaintext))).to eq plaintext + it 'returns plaintext' do + expect(described_class.decrypt(ciphertext: ciphertext)).to eq plaintext + end + end + + context 'not encrypted' do + let(:ciphertext) { plaintext } + + it 'returns plaintext' do + expect(described_class.decrypt(ciphertext: ciphertext)).to eq plaintext + end + end end end + + it 'is idempotent' do + expect(described_class.decrypt(ciphertext: described_class.encrypt(plaintext: plaintext))).to eq plaintext + end end