Proofpoint는 올해 Entra ID를 대상으로 도난당한 자격 증명을 검증했지만 성공적인 로그인은 전혀 발생하지 않은 두 건의 캠페인을 문서화했습니다. 이 기술은 한 문장으로 설명할 만큼 간단합니다: 등록된 애플리케이션과 일치하지 않는 client ID로 토큰 엔드포인트에 비밀번호를 보내고, 반환되는 오류를 읽는 것입니다. 이 오류 코드는 중요하지만, 더 큰 정보 유출은 각 코드가 인증 순서에서 어디에 위치하는지입니다.
세 가지 코드, 세 가지 다른 의미
요청은 OAuth 2.0 Resource Owner Password Credentials(ROPC) 엔드포인트로 전송됩니다: 사용자 이름, 비밀번호 및 client_id. 이 기술은 명시적으로 ROPC에 한정되며, 이를 제한하는 것이 완화 조치 중 하나입니다.
중요한 세 가지 반응:
- AADSTS50034 (
UserAccountNotFound): 계정이 디렉터리에 없습니다. - AADSTS50126 (
InvalidUserNameOrPassword): 실패를 포괄하는 일반 오류로 문서화됨. - AADSTS700016: 디렉터리에서 애플리케이션 식별자를 찾을 수 없습니다.
AADSTS50126은 잘못된 사용자 이름과 잘못된 비밀번호를 구분하지 않아야 하지만, 관찰된 동작은 이를 구분합니다. 존재하지 않는 사용자 이름에 대한 실패한 시도는 로그인 기록에 전혀 기록되지 않습니다. Entra는 사용자 이름이 유효하다고 확인된 후에만 로그 행을 작성합니다. Entra는 없는 계정에 대해 50034를, 비밀번호가 틀린 존재하는 계정에 대해 50126을 반환하는데, 이는 Microsoft 문서에서 모호하게 다루는 관찰된 동작 차이입니다. 범위도 중요합니다. 이러한 관찰은 토큰 엔드포인트의 관리되는 테넌트에 적용됩니다. 페더레이션된 테넌트는 Pass-through Authentication(PTA) 또는 ADFS를 통해 온프레미스에서 비밀번호를 검증하므로 오류 경로가 다릅니다.
로그는 처리 방식의 차이를 증명합니다:
Request | Log row written? | What the attacker learns |
|---|---|---|
|
Account not in directory (50034)
|
No |
This username doesn't exist |
|
Account exists, wrong password (50126) |
Yes, failed sign-in |
This username exists |
|
Account exists, correct password (700016) |
Yes, failed sign-in
|
This credential pair works |
AADSTS700016은 앱 등록이 손상되었음을 나타냅니다. 익숙하지 않은 클라이언트 ID로 인증 시도가 이어진 후에 나타나며, 공격자가 유효한 비밀번호를 보유하고 있음을 확인합니다.
두 캠페인
UNK_pyreq2323는 2026년 1월 14일부터 3월 초까지 AWS 인프라를 소진했습니다. 생성된 것은 70만 개 이상의 위조된 클라이언트 ID로, 합법적인 1st-party Exchange Online 애플리케이션 식별자의 마지막 6자리를 무작위화하여 생성되었습니다(예: 00000002-0000-0ff1-ce00-000000XXXXXX). 약 4,000개 테넌트의 100만 개 이상의 계정을 대상으로 했으며, 각 위조 ID는 12개 이하 계정에만 재사용한 후 폐기했습니다. Microsoft 1st-party 애플리케이션 ID는 모든 테넌트에서 사전 동의되어 있으므로, 해당 접두사를 유지하는 ID는 잡음이 아닌 정상 ID처럼 보입니다.
UNK_OutFlareAZ는 2025년 12월에 등장했으며 2026년 2월과 3월에 다시 나타나 Cloudflare 인프라를 통해 200만 명 이상의 사용자를 대상으로 370만 개의 위조된 애플리케이션 ID.
두 방법은 서로 다른 생성 방식을 사용했으며, 이 차이가 탐지에 중요합니다. pyreq2323은 실제 접두사를 유지하고 각 ID를 최대 12회 재사용한 반면, OutFlareAZ는 요청마다 새로운 무작위 UUIDv4를 생성하여 ID당 사용자 한 명씩 할당했기 때문에 재사용에 기반한 휴리스틱은 이를 놓칩니다.
(Proofpoint는 두 캠페인을 UNK_ 지정 하에 추적하며, 이는 명명된 행위자 대신 미확인 클러스터를 표시합니다.)
탐지
수비자들은 실패와 잠금 현상을 보았지만, 어떤 실패가 자격 증명 확인인지 알 수 없었습니다. 신호는 그 자리에 있었고, 그날 아침 비밀번호를 잘못 입력한 모든 사용자와 함께 있었지만, 어떤 오류 코드를 분리해야 하는지 모르면 읽을 수 없었습니다. 진정으로 보이지 않는 절반은 열거입니다. 존재하지 않는 사용자 이름은 로그 행을 전혀 생성하지 않으므로, 모든 것에 앞서 수행되는 정찰은 어떤 테넌트에도 흔적을 남기지 않습니다.
이러한 모든 요청은 대화형 로그인 로그에 기록됩니다. 이는 Microsoft의 정의에 따른 것입니다: IsInteractive는 사용자가 인증 요소를 제공하는 로그인을 포함하며, 정의에는 암호가 명시적으로 언급되어 있고 ROPC가 암호를 제출합니다.
서명은 빈 애플리케이션 이름입니다:
Client ID in request |
|
|
|---|---|---|
|
Real registered app |
populated |
populated |
|
Well-formed UUID, unregistered
|
populated
|
blank |
|
Malformed client ID |
blank |
blank |
두 번째 행이 당신이 찾는 것입니다. isnotempty(AppId) 와 isempty(AppDisplayName) 를 짝지어 잘 형성되었지만 등록되지 않은 경우를 분리하여 잘못된 데이터까지 포함하지 않도록 합니다.
중요한 쿼리는 확인된 자격 증명 유출을 반환합니다:
SigninLogs
| where TimeGenerated > ago(30d)
| where ResultType == "700016"
| where isnotempty(AppId) and isempty(AppDisplayName)
| project TimeGenerated, UserPrincipalName, AppId, IPAddress,
UserAgent, AuthenticationProtocol, ResultDescription
| order by TimeGenerated asc
각 행은 Entra에서 검증된 자격 증명 쌍이므로 출력물을 비밀번호 재설정 및 세션 해제 목록으로 처리하세요. 이는 앱 등록 티켓이 아니며, 여기서 가장 심각한 실수는 700016건의 접근을 앱 등록 소유자에게 전달하는 것입니다.
캠페인 탐지를 위해, 클라이언트 ID 대신 소스를 기준으로 전환하세요:
SigninLogs
| where TimeGenerated > ago(7d)
| where isnotempty(AppId) and isempty(AppDisplayName)
| summarize
DistinctAppIds = dcount(AppId),
TargetedUsers = dcount(UserPrincipalName),
ConfirmedCreds = countif(ResultType == "700016"),
WrongPassword = countif(ResultType == "50126"),
Lockouts = countif(ResultType == "50053"),
UserAgents = make_set(UserAgent, 10)
by IPAddress, bin(TimeGenerated, 1h)
| where DistinctAppIds > 10
| order by DistinctAppIds desc
IPAddress에 대한 피벗은 IPAddress 의도적입니다. 12계정 재사용 한도에 기반한 모든 휴리스틱은 pyreq2323을 식별하지만 OutFlareAZ는 전혀 감지하지 못하는데, 두 번째는 절대 ID를 재사용하지 않기 때문입니다. 두 캠페인은 출처에서 동일하게 보입니다: 많은 수의 클라이언트 ID를 제시하지만 아무것도 해결하지 못하는 하나의 출처입니다. 같은 행에 ConfirmedCreds 와 Lockouts 을 유지하면 실제 침해 옆의 노이즈를 보여줍니다.
수정 사항이 포함하는 내용과 포함하지 않는 내용
Conditional Access를 통해 ROPC를 제한하는 것은 올바른 조치이며 Proofpoint에서 권장합니다. 하지만 이것이 무엇을 의미하는지 명확히 하고 싶습니다.
열거에는 영향을 미치지 않습니다. Entra가 사용자 이름을 확인할 때 AADSTS50034가 반환되며, 이는 자격 증명 검증 이전이며 정책 평가보다 두 단계 앞선 시점입니다. Conditional Access 정책은 정책이 참조되기 전에 발행된 응답을 가로챌 수 없으며, 테넌트 구성과 관계없이 로그 행도 기록되지 않습니다. 디렉터리에 대한 사용자 이름 열거는 강력한 레거시 인증 차단을 포함하여 모든 테넌트 측 제어를 통과합니다.
레거시 인증을 차단하는 정책은 AADSTS53003을 반환합니다, BlockedByConditionalAccess. 동일한 순서 논리에 따라, 53003은 비밀번호가 검증된 후에만 도달할 수 있으므로 공격자의 오류 메시지는 53003 대신 50126이 됩니다. 다른 코드, 동일한 정보입니다.
참고문헌
공유하기
더 알아보기
저자 소개
Darryl Baker
수석 스태프 보안 연구원
Darryl G. Baker는 Netwrix의 수석 스태프 보안 연구원이며 Identity 및 Active Directory 보안 분야에서 널리 인정받는 권위자입니다. 10년이 넘는 아이덴티티 시스템 경험을 바탕으로 그는 Active Directory, Entra ID, Azure 환경에 초점을 둔 엔터프라이즈 보안 평가, 아이덴티티 보안 교육, 위협 에뮬레이션을 이끌어 왔습니다. Darryl은 BlueTeamCon, BSidesCT, The Experts Conference, Wild Wild West Hackin’ Fest에서 높은 평가를 받은 교육과 데모를 제공해 왔습니다. 그는 현재의 레드팀 및 블루팀 도구를 활용해 공격 경로 분석부터 위협 사냥(threat hunting)까지 수비 담당자가 모든 역량을 익힐 수 있도록 돕는 수많은 핸즈온 공격 에뮬레이션 랩의 설계자입니다. 그의 세션에서는 Darryl이 깊이 있는 기술 통찰과 실제 사례 연구를 결합하여, 블루팀 전문가들이 Identity 보안 자세를 강화하고 진화하는 공격자 기법에 대비해 방어할 수 있도록 역량을 높여 줍니다.