Add validation functionality to FlaskUnsign

This commit is contained in:
Spencer McIntyre 2023-09-07 15:58:14 -04:00
parent 042136cf57
commit 143e1c82b5
3 changed files with 68 additions and 38 deletions

View File

@ -15,7 +15,7 @@ module Msf
Base64.urlsafe_encode64(value).gsub(/=+$/, '')
end
class Signer
class URLSafeSigner
def initialize(secret_key, salt)
@secret_key = secret_key
@salt = salt
@ -34,7 +34,7 @@ module Msf
end
end
class TimestampSigner < Signer
class URLSafeTimedSigner < URLSafeSigner
SEPARATOR = '.'
def get_timestamp
@ -47,29 +47,49 @@ module Msf
def sign(value)
timestamp = [get_timestamp].pack('Q>')
timestamp.delete_prefix!("\x00") while timestamp.start_with?("\x00")
timestamp.delete_prefix!("\x00".b) while timestamp.start_with?("\x00".b)
timestamp = FlaskUnsign.base64_encode(timestamp)
value = value + SEPARATOR + timestamp
value + SEPARATOR + get_signature(value)
end
def valid?(value)
value, _, signature = value.rpartition(SEPARATOR)
value, _, timestamp = value.rpartition(SEPARATOR)
signature == get_signature(value + SEPARATOR + timestamp)
end
end
# This emulates the default cookie-based session storage used by the latest version of Flask as of the time of
# this writing (2023-09-07).
# See: https://github.com/pallets/flask/blob/8037487165a196015a646de25cbce6d0351c8fc4/src/flask/sessions.py#L276
module Session
DEFAULT_SALT = 'cookie-session'
def self.decode(value)
parse(value)[:deserialized]
end
def self.parse(value)
compressed = value.start_with?('.')
value = value[1..] if compressed
value = value.split('.', 2).first
value = Base64.urlsafe_decode64(value)
serialized, signature = value.split('.', 3)
value = Base64.urlsafe_decode64(serialized)
value = Zlib::Inflate.inflate(value) if compressed
JSON.parse(value)
{ compressed: compressed, signature: signature, deserialized: JSON.parse(value), serialized: serialized }
end
def self.sign(value, secret, salt: 'cookie-session')
def self.sign(value, secret, salt: DEFAULT_SALT)
json = JSON.dump(value)
signer = TimestampSigner.new(secret, salt)
signer = URLSafeTimedSigner.new(secret, salt)
signer.sign(FlaskUnsign.base64_encode(json).strip)
end
def self.valid?(value, secret, salt: DEFAULT_SALT)
signer = URLSafeTimedSigner.new(secret, salt)
signer.valid?(value)
end
end
end
end

View File

@ -1,30 +0,0 @@
require 'spec_helper'
RSpec.describe Msf::Exploit::Remote::HTTP::FlaskUnsign do
subject do
mod = Msf::Exploit.new
mod.extend(Msf::Exploit::Remote::HTTP::FlaskUnsign)
mod
end
describe '#flask_unsign' do
context 'correctly decodes cookie' do
it 'returns a hash' do
expect(Msf::Exploit::Remote::HTTP::FlaskUnsign::Session.decode('eyJoZWxsbyI6IndvcmxkIn0.XDtqeQ.1qsBdjyRJLokwRzJdzXMVCSyRTA')).to eql({ 'hello' => 'world' })
end
# derived from logged in session from Apache Supserset
it 'returns a hash from complex dict' do
expect(Msf::Exploit::Remote::HTTP::FlaskUnsign::Session.decode('.eJwlj0GKAzEMBP-i8xwsW7alfGawZIkNGTYwk5yW_D2GvTZdVPcf7HH69QO31_n2Dfb7hBvwrBJRJ3OTUM29GI6SI4tN9MpC2IWmo4Ya9qxVjVJNI880syOpWmPX1oxXLYdOSVlKipXhHJHqcNEWrNSJkbtx71gGIkVIwAZ2nbG_ng__XXuWl1JwNBStXRNr8UZqA6M39aXiImi4uONp4_DFLHCD9-Xn_yWEzxfWdkQs.ZKXFig.tOBl4_CxT7zWg3EaZZNce7NP4rc')).to eql({"_fresh"=>true, "_id"=>"8d59ff5d8869fbb273c1a32f29cd1e58941794de1bfbc172b5bc4050a2d0d2e14bbc68eb66c84de2fbd902930feb61daf05ae9b6f8b4748187c87713a114ff9f", "csrf_token"=>"29c40f8f619b57b08b3e64bca1f76be68e8391c1", "locale"=>"en", "user_id"=>"1"})
end
end
context 'correctly signs decoded cookie' do
it 'returns a cookie string' do
@freezed_time = Time.utc(2023, 7, 10, 12, 0, 0)
allow(Time).to receive(:now).and_return(@freezed_time)
expect(Msf::Exploit::Remote::HTTP::FlaskUnsign::Session.sign({ 'hello' => 'world' }, 'CHANGEME')).to eql('eyJoZWxsbyI6IndvcmxkIn0.ZKvywA.s78heXzx4hJKO55wwu5X7RiS164')
end
end
end
end

View File

@ -0,0 +1,40 @@
require 'spec_helper'
RSpec.describe Msf::Exploit::Remote::HTTP::FlaskUnsign::Session do
let(:secret) { 'CHANGEME' }
describe '.decode' do
it 'returns a hash' do
expect(Msf::Exploit::Remote::HTTP::FlaskUnsign::Session.decode('eyJoZWxsbyI6IndvcmxkIn0.XDtqeQ.1qsBdjyRJLokwRzJdzXMVCSyRTA')).to eql({ 'hello' => 'world' })
end
# derived from logged in session from Apache Supserset
it 'returns a hash from complex dict' do
expected = {
"_fresh" => true,
"_id" => "8d59ff5d8869fbb273c1a32f29cd1e58941794de1bfbc172b5bc4050a2d0d2e14bbc68eb66c84de2fbd902930feb61daf05ae9b6f8b4748187c87713a114ff9f",
"csrf_token" => "29c40f8f619b57b08b3e64bca1f76be68e8391c1",
"locale" => "en",
"user_id" => "1"
}
expect(Msf::Exploit::Remote::HTTP::FlaskUnsign::Session.decode('.eJwlj0GKAzEMBP-i8xwsW7alfGawZIkNGTYwk5yW_D2GvTZdVPcf7HH69QO31_n2Dfb7hBvwrBJRJ3OTUM29GI6SI4tN9MpC2IWmo4Ya9qxVjVJNI880syOpWmPX1oxXLYdOSVlKipXhHJHqcNEWrNSJkbtx71gGIkVIwAZ2nbG_ng__XXuWl1JwNBStXRNr8UZqA6M39aXiImi4uONp4_DFLHCD9-Xn_yWEzxfWdkQs.ZKXFig.tOBl4_CxT7zWg3EaZZNce7NP4rc')).to eql(expected)
end
end
describe '.sign' do
it 'returns a cookie string' do
@freezed_time = Time.utc(2023, 7, 10, 12, 0, 0)
allow(Time).to receive(:now).and_return(@freezed_time)
expect(Msf::Exploit::Remote::HTTP::FlaskUnsign::Session.sign({ 'hello' => 'world' }, secret)).to eql('eyJoZWxsbyI6IndvcmxkIn0.ZKvywA.s78heXzx4hJKO55wwu5X7RiS164')
end
end
describe '.valid?' do
it 'verifies a signed cookie' do
expect(Msf::Exploit::Remote::HTTP::FlaskUnsign::Session.valid?('eyJoZWxsbyI6IndvcmxkIn0.ZKvywA.s78heXzx4hJKO55wwu5X7RiS164', secret)).to be true
end
it 'does not verify an invalid signed cookie' do
expect(Msf::Exploit::Remote::HTTP::FlaskUnsign::Session.valid?('eyJoZWxsbyI6IndvcmxkIn0.ZKvywA.s78heXzx4hJKO55wwu5X7RiS163', secret)).to be false
end
end
end