Add validation functionality to FlaskUnsign
This commit is contained in:
parent
042136cf57
commit
143e1c82b5
|
@ -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
|
||||
|
|
|
@ -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
|
|
@ -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
|
Loading…
Reference in New Issue