%% kerlberos %% %% Copyright 2020 Alex Wilson %% The University of Queensland %% All rights reserved. %% %% Redistribution and use in source and binary forms, with or without %% modification, are permitted provided that the following conditions %% are met: %% 1. Redistributions of source code must retain the above copyright %% notice, this list of conditions and the following disclaimer. %% 2. Redistributions in binary form must reproduce the above copyright %% notice, this list of conditions and the following disclaimer in the %% documentation and/or other materials provided with the distribution. %% %% THIS SOFTWARE IS PROVIDED BY THE AUTHOR ``AS IS'' AND ANY EXPRESS OR %% IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES %% OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. %% IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY DIRECT, INDIRECT, %% INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT %% NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, %% DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY %% THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT %% (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF %% THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. %% %% @doc Functions for decoding Microsoft PACs inside a Kerberos ticket. -module(krb_ms_pac). -export([decode/1, decode_ticket/1]). %% @headerfile "ms_pac.hrl" -include("ms_pac.hrl"). -include("KRB5.hrl"). -export_type([pac/0, sid/0, sid_attr/0, pac_buffer/0]). -type sid_attr() :: mandatory | default | enabled | owner | resource. -type sid() :: #sid{}. -type pac() :: #pac{}. -type pac_unknown() :: #pac_unknown{}. -type pac_client_info() :: #pac_client_info{}. -type pac_upn_dns() :: #pac_upn_dns{}. -type pac_logon_info() :: #pac_logon_info{}. -type pac_buffer() :: pac_unknown() | pac_client_info() | pac_upn_dns() | pac_logon_info(). %% @doc Extracts and decodes a PAC from a given Kerberos ticket. -spec decode_ticket(#'Ticket'{}) -> {ok, pac()} | {error, no_pac} | {error, {bad_pac, term()}}. decode_ticket(#'Ticket'{'enc-part' = ETP = #'EncTicketPart'{}}) -> #'EncTicketPart'{'authorization-data' = ADs} = ETP, find_pac_ad(ADs). find_pac_ad(asn1_NOVALUE) -> {error, no_pac}; find_pac_ad([]) -> {error, no_pac}; find_pac_ad([#'AuthorizationData_SEQOF'{'ad-type' = 128} = AD0 | _]) -> #'AuthorizationData_SEQOF'{'ad-data' = D0} = AD0, case (catch decode(D0)) of {'EXIT', Why} -> {error, {bad_pac, Why}}; Pac = #pac{} -> {ok, Pac} end; find_pac_ad([#'AuthorizationData_SEQOF'{'ad-type' = 1} = AD0 | Rest]) -> #'AuthorizationData_SEQOF'{'ad-data' = D0} = AD0, case krb_proto:decode(D0, ['AuthorizationData']) of {ok, InnerAD} -> find_pac_ad(Rest ++ InnerAD); Err -> Err end; find_pac_ad([_ | Rest]) -> find_pac_ad(Rest). %% @doc Decodes a PAC from raw bytes. Throws errors on invalid input. -spec decode(binary()) -> pac(). decode(Bin = <>) -> Bufs = decode_info_bufs(Count, Rem, Bin), #pac{version = Version, buffers = Bufs}. decode_info_bufs(0, _, _Bin) -> []; decode_info_bufs(N, <>, Bin) -> <<_:Offset/binary, Segment:Size/binary, _/binary>> = Bin, Rec = case Type of 16#01 -> decode_logon_info(Segment, Bin); 16#0a -> decode_client_info(Segment, Bin); 16#0c -> decode_upn_dns(Segment, Bin); % 16#0e -> decode_device_info(Segment, Bin); % 16#0d -> decode_client_claims(Segment, Bin); % 16#0f -> decode_device_claims(Segment, Bin); _ -> #pac_unknown{type = Type, data = Segment} end, [Rec | decode_info_bufs(N - 1, Rem, Bin)]. decode_client_info(Segment, _Bin) -> <> = Segment, #pac_client_info{ tgt_time = TgtTime, name = unicode:characters_to_binary(Name, {utf16, little}, utf8) }. decode_upn_dns(Segment, _Bin) -> <> =Segment, <<_:UpnOffset/binary, Upn:UpnLen/binary, _/binary>> = Segment, <<_:DnsNameOffset/binary, DnsName:DnsNameLen/binary, _/binary>> = Segment, #pac_upn_dns{ upn = unicode:characters_to_binary(Upn, {utf16, little}, utf8), dns_name = unicode:characters_to_binary(DnsName, {utf16, little}, utf8) }. decode_logon_info(Segment, _Bin) -> S0 = ms_rpce:start(Segment), {LogonTime, S1} = ms_rpce:read(filetime, S0), {LogoffTime, S2} = ms_rpce:read(filetime, S1), {KickOffTime, S3} = ms_rpce:read(filetime, S2), {PasswordLastSet, S4} = ms_rpce:read(filetime, S3), {PasswordCanChange, S5} = ms_rpce:read(filetime, S4), {PasswordMustChange, S6} = ms_rpce:read(filetime, S5), {EffNamePtr, S7} = ms_rpce:read(rpc_unicode_string, S6), {FullNamePtr, S8} = ms_rpce:read(rpc_unicode_string, S7), {LogonScriptPtr, S9} = ms_rpce:read(rpc_unicode_string, S8), {ProfilePathPtr, S10} = ms_rpce:read(rpc_unicode_string, S9), {HomeDirPtr, S11} = ms_rpce:read(rpc_unicode_string, S10), {HomeDirDrivePtr, S12} = ms_rpce:read(rpc_unicode_string, S11), {LogonCount, S13} = ms_rpce:read(ushort, S12), {BadPasswordCount, S14} = ms_rpce:read(ushort, S13), {UserId, S15} = ms_rpce:read(ulong, S14), {_PrimaryGroupId, S16} = ms_rpce:read(ulong, S15), {_GroupCount, S17} = ms_rpce:read(ulong, S16), {GroupsPtr, S18} = ms_rpce:read({pointer, {array, group_membership}}, S17), {_UserFlags, S19} = ms_rpce:read(ulong, S18), {_SessKey, S20} = ms_rpce:read(user_session_key, S19), {LogonServerPtr, S21} = ms_rpce:read(rpc_unicode_string, S20), {LogonDomainNamePtr, S22} = ms_rpce:read(rpc_unicode_string, S21), {LogonDomainIdPtr, S23} = ms_rpce:read({pointer, sid}, S22), {_Reserved1, S24} = ms_rpce:read(ulong, S23), {_Reserved2, S25} = ms_rpce:read(ulong, S24), {_UAC, S26} = ms_rpce:read(ulong, S25), {_SubAuthStatus, S27} = ms_rpce:read(ulong, S26), {_LastSuccessfulILogon, S28} = ms_rpce:read(filetime, S27), {_LastFailedILogon, S29} = ms_rpce:read(filetime, S28), {_FailedILogonCount, S30} = ms_rpce:read(ulong, S29), {_Reserved3, S31} = ms_rpce:read(ulong, S30), {_SidCount, S32} = ms_rpce:read(ulong, S31), {SidPtr, S33} = ms_rpce:read({pointer, {array, kerb_sid_and_attributes}}, S32), {_RscGroupDomainSid, S34} = ms_rpce:read({pointer, sid}, S33), {_RscGroupCount, S35} = ms_rpce:read(ulong, S34), {_RscGroupPtr, S36} = ms_rpce:read({pointer, {array, group_membership}}, S35), SFinal = ms_rpce:finish(S36), Sids0 = ms_rpce:get_ptr(SidPtr, SFinal), Sids1 = [S#sid_and_attributes{ sid = ms_rpce:get_ptr(SidPtr, SFinal)} || S = #sid_and_attributes{sid_ptr = ASidPtr} <- Sids0, ASidPtr =:= SidPtr], #pac_logon_info{ times = #{ logon => LogonTime, logoff => LogoffTime, kickoff => KickOffTime, pw_last_set => PasswordLastSet, pw_can_change => PasswordCanChange, pw_must_change => PasswordMustChange }, ename = ms_rpce:get_ptr(EffNamePtr, SFinal), fname = ms_rpce:get_ptr(FullNamePtr, SFinal), logon_script = ms_rpce:get_ptr(LogonScriptPtr, SFinal), profile_path = ms_rpce:get_ptr(ProfilePathPtr, SFinal), homedir = ms_rpce:get_ptr(HomeDirPtr, SFinal), home_drive = ms_rpce:get_ptr(HomeDirDrivePtr, SFinal), logon_count = LogonCount, bad_pw_count = BadPasswordCount, userid = UserId, groups = ms_rpce:get_ptr(GroupsPtr, SFinal), logon_server = ms_rpce:get_ptr(LogonServerPtr, SFinal), domain = ms_rpce:get_ptr(LogonDomainNamePtr, SFinal), domain_sid = ms_rpce:get_ptr(LogonDomainIdPtr, SFinal), sids = Sids1 }.