1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 274 275 276 277 278 279 280 281 282 283 284 285 286 287 288 289 290 291 292 293 294 295 296 297 298 299 300 301 302 303 304 305 306 307 308 309 310 311 312 313 314 315 316 317 318 319 320 321 322 323 324 325 326 327 328 329 330 331
|
# frozen_string_literal: false
#
# = win32/sspi.rb
#
# Copyright (c) 2006-2007 Justin Bailey
#
# Written and maintained by Justin Bailey <jgbailey@gmail.com>.
#
# This program is free software. You can re-distribute and/or
# modify this program under the same terms of ruby itself ---
# Ruby Distribution License or GNU General Public License.
#
require 'Win32API'
# Implements bindings to Win32 SSPI functions, focused on authentication to a proxy server over HTTP.
module Win32
module SSPI
# Specifies how credential structure requested will be used. Only SECPKG_CRED_OUTBOUND is used
# here.
SECPKG_CRED_INBOUND = 0x00000001
SECPKG_CRED_OUTBOUND = 0x00000002
SECPKG_CRED_BOTH = 0x00000003
# Format of token. NETWORK format is used here.
SECURITY_NATIVE_DREP = 0x00000010
SECURITY_NETWORK_DREP = 0x00000000
# InitializeSecurityContext Requirement flags
ISC_REQ_REPLAY_DETECT = 0x00000004
ISC_REQ_SEQUENCE_DETECT = 0x00000008
ISC_REQ_CONFIDENTIALITY = 0x00000010
ISC_REQ_USE_SESSION_KEY = 0x00000020
ISC_REQ_PROMPT_FOR_CREDS = 0x00000040
ISC_REQ_CONNECTION = 0x00000800
# Win32 API Functions. Uses Win32API to bind methods to constants contained in class.
module API
# Can be called with AcquireCredentialsHandle.call()
AcquireCredentialsHandle = Win32API.new("secur32", "AcquireCredentialsHandle", 'ppLpppppp', 'L')
# Can be called with InitializeSecurityContext.call()
InitializeSecurityContext = Win32API.new("secur32", "InitializeSecurityContext", 'pppLLLpLpppp', 'L')
# Can be called with DeleteSecurityContext.call()
DeleteSecurityContext = Win32API.new("secur32", "DeleteSecurityContext", 'P', 'L')
# Can be called with FreeCredentialsHandle.call()
FreeCredentialsHandle = Win32API.new("secur32", "FreeCredentialsHandle", 'P', 'L')
end
# SecHandle struct
class SecurityHandle
def upper
@struct.unpack("LL")[1]
end
def lower
@struct.unpack("LL")[0]
end
def to_p
@struct ||= "\0" * 8
end
end
# Some familiar aliases for the SecHandle structure
CredHandle = CtxtHandle = SecurityHandle
# TimeStamp struct
class TimeStamp
attr_reader :struct
def to_p
@struct ||= "\0" * 8
end
end
# Creates binary representations of a SecBufferDesc structure,
# including the SecBuffer contained inside.
class SecurityBuffer
SECBUFFER_TOKEN = 2 # Security token
TOKENBUFSIZE = 12288
SECBUFFER_VERSION = 0
def initialize(buffer = nil)
@buffer = buffer || "\0" * TOKENBUFSIZE
@bufferSize = @buffer.length
@type = SECBUFFER_TOKEN
end
def bufferSize
unpack
@bufferSize
end
def bufferType
unpack
@type
end
def token
unpack
@buffer
end
def to_p
# Assumption is that when to_p is called we are going to get a packed structure. Therefore,
# set @unpacked back to nil so we know to unpack when accessors are next accessed.
@unpacked = nil
# Assignment of inner structure to variable is very important here. Without it,
# will not be able to unpack changes to the structure. Alternative, nested unpacks,
# does not work (i.e. @struct.unpack("LLP12")[2].unpack("LLP12") results in "no associated pointer")
@sec_buffer ||= [@bufferSize, @type, @buffer].pack("LLP")
@struct ||= [SECBUFFER_VERSION, 1, @sec_buffer].pack("LLP")
end
private
# Unpacks the SecurityBufferDesc structure into member variables. We
# only want to do this once per struct, so the struct is deleted
# after unpacking.
def unpack
if ! @unpacked && @sec_buffer && @struct
@bufferSize, @type = @sec_buffer.unpack("LL")
@buffer = @sec_buffer.unpack("LLP#{@bufferSize}")[2]
@struct = nil
@sec_buffer = nil
@unpacked = true
end
end
end
# SEC_WINNT_AUTH_IDENTITY structure
class Identity
SEC_WINNT_AUTH_IDENTITY_ANSI = 0x1
attr_accessor :user, :domain, :password
def initialize(user = nil, domain = nil, password = nil)
@user = user
@domain = domain
@password = password
@flags = SEC_WINNT_AUTH_IDENTITY_ANSI
end
def to_p
[@user, @user ? @user.length : 0,
@domain, @domain ? @domain.length : 0,
@password, @password ? @password.length : 0,
@flags].pack("PLPLPLL")
end
end
# Takes a return result from an SSPI function and interprets the value.
class SSPIResult
# Good results
SEC_E_OK = 0x00000000
SEC_I_CONTINUE_NEEDED = 0x00090312
# These are generally returned by InitializeSecurityContext
SEC_E_INSUFFICIENT_MEMORY = 0x80090300
SEC_E_INTERNAL_ERROR = 0x80090304
SEC_E_INVALID_HANDLE = 0x80090301
SEC_E_INVALID_TOKEN = 0x80090308
SEC_E_LOGON_DENIED = 0x8009030C
SEC_E_NO_AUTHENTICATING_AUTHORITY = 0x80090311
SEC_E_NO_CREDENTIALS = 0x8009030E
SEC_E_TARGET_UNKNOWN = 0x80090303
SEC_E_UNSUPPORTED_FUNCTION = 0x80090302
SEC_E_WRONG_PRINCIPAL = 0x80090322
# These are generally returned by AcquireCredentialsHandle
SEC_E_NOT_OWNER = 0x80090306
SEC_E_SECPKG_NOT_FOUND = 0x80090305
SEC_E_UNKNOWN_CREDENTIALS = 0x8009030D
@@map = {}
constants.each { |v| @@map[self.const_get(v.to_s)] = v }
attr_reader :value
def initialize(value)
# convert to unsigned long
value = [value].pack("L").unpack("L").first
raise "#{value.to_s(16)} is not a recognized result" unless @@map.has_key? value
@value = value
end
def to_s
@@map[@value].to_s
end
def ok?
@value == SEC_I_CONTINUE_NEEDED || @value == SEC_E_OK
end
def ==(other)
if other.is_a?(SSPIResult)
@value == other.value
elsif other.is_a?(Fixnum)
@value == @@map[other]
else
false
end
end
end
# Handles "Negotiate" type authentication. Geared towards authenticating with a proxy server over HTTP
class NegotiateAuth
attr_accessor :credentials, :context, :contextAttributes, :user, :domain
# Default request flags for SSPI functions
REQUEST_FLAGS = ISC_REQ_CONFIDENTIALITY | ISC_REQ_REPLAY_DETECT | ISC_REQ_CONNECTION
# NTLM tokens start with this header always. Encoding alone adds "==" and newline, so remove those
B64_TOKEN_PREFIX = ["NTLMSSP"].pack("m").delete("=\n")
# Given a connection and a request path, performs authentication as the current user and returns
# the response from a GET request. The connnection should be a Net::HTTP object, and it should
# have been constructed using the Net::HTTP.Proxy method, but anything that responds to "get" will work.
# If a user and domain are given, will authenticate as the given user.
# Returns the response received from the get method (usually Net::HTTPResponse)
def NegotiateAuth.proxy_auth_get(http, path, user = nil, domain = nil)
raise "http must respond to :get" unless http.respond_to?(:get)
nego_auth = self.new user, domain
resp = http.get path, { "Proxy-Authorization" => "Negotiate " + nego_auth.get_initial_token }
if resp["Proxy-Authenticate"]
resp = http.get path, { "Proxy-Authorization" => "Negotiate " + nego_auth.complete_authentication(resp["Proxy-Authenticate"].split(" ").last.strip) }
end
resp
end
# Creates a new instance ready for authentication as the given user in the given domain.
# Defaults to current user and domain as defined by ENV["USERDOMAIN"] and ENV["USERNAME"] if
# no arguments are supplied.
def initialize(user = nil, domain = nil)
if user.nil? && domain.nil? && ENV["USERNAME"].nil? && ENV["USERDOMAIN"].nil?
raise "A username or domain must be supplied since they cannot be retrieved from the environment"
end
@user = user || ENV["USERNAME"]
@domain = domain || ENV["USERDOMAIN"]
end
# Gets the initial Negotiate token. Returns it as a base64 encoded string suitable for use in HTTP. Can
# be easily decoded, however.
def get_initial_token
raise "This object is no longer usable because its resources have been freed." if @cleaned_up
get_credentials
outputBuffer = SecurityBuffer.new
@context = CtxtHandle.new
@contextAttributes = "\0" * 4
result = SSPIResult.new(API::InitializeSecurityContext.call(@credentials.to_p, nil, nil,
REQUEST_FLAGS,0, SECURITY_NETWORK_DREP, nil, 0, @context.to_p, outputBuffer.to_p, @contextAttributes, TimeStamp.new.to_p))
if result.ok? then
return encode_token(outputBuffer.token)
else
raise "Error: #{result.to_s}"
end
end
# Takes a token and gets the next token in the Negotiate authentication chain. Token can be Base64 encoded or not.
# The token can include the "Negotiate" header and it will be stripped.
# Does not indicate if SEC_I_CONTINUE or SEC_E_OK was returned.
# Token returned is Base64 encoded w/ all new lines removed.
def complete_authentication(token)
raise "This object is no longer usable because its resources have been freed." if @cleaned_up
# Nil token OK, just set it to empty string
token = "" if token.nil?
if token.include? "Negotiate"
# If the Negotiate prefix is passed in, assume we are seeing "Negotiate <token>" and get the token.
token = token.split(" ").last
end
if token.include? B64_TOKEN_PREFIX
# indicates base64 encoded token
token = token.strip.unpack("m")[0]
end
outputBuffer = SecurityBuffer.new
result = SSPIResult.new(API::InitializeSecurityContext.call(@credentials.to_p, @context.to_p, nil,
REQUEST_FLAGS, 0, SECURITY_NETWORK_DREP, SecurityBuffer.new(token).to_p, 0,
@context.to_p,
outputBuffer.to_p, @contextAttributes, TimeStamp.new.to_p))
if result.ok? then
return encode_token(outputBuffer.token)
else
raise "Error: #{result.to_s}"
end
ensure
# need to make sure we don't clean up if we've already cleaned up.
clean_up unless @cleaned_up
end
private
def clean_up
# free structures allocated
@cleaned_up = true
API::FreeCredentialsHandle.call(@credentials.to_p)
API::DeleteSecurityContext.call(@context.to_p)
@context = nil
@credentials = nil
@contextAttributes = nil
end
# Gets credentials based on user, domain or both. If both are nil, an error occurs
def get_credentials
@credentials = CredHandle.new
ts = TimeStamp.new
@identity = Identity.new @user, @domain
result = SSPIResult.new(API::AcquireCredentialsHandle.call(nil, "Negotiate", SECPKG_CRED_OUTBOUND, nil, @identity.to_p,
nil, nil, @credentials.to_p, ts.to_p))
raise "Error acquire credentials: #{result}" unless result.ok?
end
def encode_token(t)
# encode64 will add newlines every 60 characters so we need to remove those.
[t].pack("m").delete("\n")
end
end
end
end
|