ARTICLE / 安全

数字身份与访问管理安全取证深度分析

数字身份与访问管理(Digital Identity and Access Management, DIAM)是现代企业安全架构的核心支柱。从云端 IAM 策略到 OAuth/OIDC 单点登录,从多因素认证到特权访问管理,身份系统构成了信任链的根基。一旦身份系统被攻破,攻击者即可获得合法凭据的掩护,大幅降低被检测的概率。

近年来,针对身份系统的攻击呈爆发式增长。2020 年 SolarWinds 事件中攻击者伪造 SAML 令牌实现持久化,2022 年 Lapsus$ 组织通过社会工程绕过 Okta MFA 入侵多家企业,Azure AD Golden Ticket 攻击利用 Kerberos 协议弱点实现无密码域控。这些攻击的共同特点是:攻击者将身份系统本身作为攻击目标,利用身份基础设施的信任传递机制扩大战果。

对于取证分析人员而言,身份安全事件的分析面临独特挑战:身份日志分散在多个系统中(IdP、RADIUS、云平台、应用层),需要跨域关联;令牌和断言是瞬态数据,取证窗口有限;合法凭据与攻击者的操作难以区分。本文系统性地梳理 DIAM 安全事件的取证分析方法论,提供可落地的检测规则和分析工具。


0x01 技术基础与 DIAM 架构概述

IAM 核心架构模型

现代身份与访问管理系统由多个层次组成,每一层都可能成为攻击者的入侵点。理解 DIAM 架构是进行身份安全取证的前提。

架构层次核心组件功能定位代表产品/标准
身份存储层LDAP、Active Directory、Directory-as-a-Service存储用户身份属性和凭据AD、Azure AD、Okta Universal Directory
认证层认证服务器、MFA 服务、FIDO2验证用户身份真实性Keycloak、Azure AD MFA、Duo Security
授权层策略引擎、RBAC/ABAC 管理器决定已认证用户的访问权限AWS IAM、Azure RBAC、OPA
联邦层IdP、SP、SAML/OIDC 中继跨域身份传递和信任建立ADFS、PingFederate、Auth0
会话管理层Token Service、Session Store管理认证状态的生命周期OAuth 2.0 Authorization Server、Redis Session
审计层日志收集、SIEM、UEBA记录和分析身份相关事件Splunk、Microsoft Sentinel、Google Chronicle

身份生命周期与攻击面映射

用户的数字身份从创建到销毁经历完整生命周期,每个阶段都存在对应的安全风险。

生命周期阶段安全事件类型MITRE ATT&CK典型攻击手法
供应(Provisioning)身份伪造、权限过度分配T1136.003创建后门账户、配置过度宽松的 RBAC 策略
认证(Authentication)凭据窃取、MFA 绕过T1556、T1621凭据填充、SIM Swap、Session Fixation
授权(Authorization)权限提升、横向移动T1078、T1098云 IAM 策略篡改、角色假设滥用
会话管理(Session)令牌劫持、会话固定T1539、T1550.004OAuth 授权码注入、Cookie 伪造
联邦信任(Federation)断言伪造、信任链劫持T1550.003SAML 断言篡改、Kerberoasting
审计/注销(De-provisioning)隐蔽持久化、幽灵账户T1136.001禁用而非删除账户、服务账户复活

攻击面分类体系

基于攻击者对身份系统的利用方式,可以将 DIAM 攻击面划分为以下类别:

攻击面类别攻击目标危害等级取证难度
云 IAM 配置滥用IAM 策略、角色、信任关系🔴 高中等
OAuth/OIDC 协议攻击授权流程、令牌签发🔴 高较高
MFA 绕过与会话劫持认证因子、会话令牌🔴 高较高
PAM 系统攻击特权凭据、密码保险箱🔴 高中等
身份联邦攻击SAML 断言、Kerberos 票据🔴 高
凭据存储攻击密码哈希、密钥库🟡 中中等
目录服务攻击LDAP 绑定、组策略🟡 中中等

取证工具链

身份安全取证需要一套专门化的工具链,覆盖日志分析、令牌解析、策略审计和行为基线建模等多个环节。

工具名称功能定位适用场景获取方式
CloudTrail + AthenaAWS 身份事件查询与分析云 IAM 操作审计AWS Console / CLI
Azure AD Audit LogsAzure AD 身份事件日志云身份认证/授权审计Azure Portal / Graph API
ScoutSuite多云 IAM 安全审计云 IAM 配置基线检查pip install scoutsuite
ProwlerAWS 安全最佳实践检查AWS IAM 策略合规审计CLI 二进制
BloodHound / SharpHoundAD 攻击路径分析Active Directory 攻击面映射GitHub Release
ImpacketWindows 协议攻击工具集Kerberos 攻击、NTLM 中继pip install impacket
jwt_toolJWT/OAuth 令牌测试JWT 伪造、算法混淆攻击pip install jwt_tool
SAML RaiderSAML 断言篡改SAML 安全测试Burp Suite 插件
Sigma通用检测规则引擎身份攻击日志检测规则GitHub 仓库
Plaso/log2timeline时间线分析多源身份日志时间线整合pip install plaso
Elastic SecuritySIEM 与行为分析身份异常行为检测Elastic Stack
CyberArk Conjur特权凭据管理审计PAM 系统安全检查商业产品

0x02 云 IAM 配置滥用与权限提升取证

AWS IAM 配置审计

AWS IAM 是最广泛使用的云身份管理系统之一。攻击者通常通过枚举过度宽松的 IAM 策略来寻找提权路径。

AWS IAM 权限边界检查:

aws iam list-policies --scope Local --query 'Policies[].{Name:PolicyName,Arn:Arn}' --output table

aws iam simulate-principal-policy \
  --policy-source-arn arn:aws:iam::123456789012:role/EC2-Role \
  --action-names iam:CreateUser iam:AttachUserPolicy iam:CreateAccessKey \
  --output json | jq '.EvaluationResults[] | {Action:EvalActionName, Effect:EvalPolicyEffect}'

IAM 角色信任关系审计:

aws iam list-roles --query 'Roles[].{RoleName:RoleName,AssumeRolePolicyDocument:AssumeRolePolicyDocument}' --output json | \
python3 -c "
import json, sys
for role in json.load(sys.stdin):
    doc = role['AssumeRolePolicyDocument']
    for stmt in doc.get('Statement', []):
        if stmt.get('Effect') == 'Allow':
            principal = stmt.get('Principal', {})
            if '*' in str(principal):
                print(f'[CRITICAL] Wildcard trust: {role[\"RoleName\"]}')
            elif 'AWS' in principal:
                aws_prin = principal['AWS']
                if isinstance(aws_prin, str):
                    aws_prin = [aws_prin]
                for p in aws_prin:
                    if 'root' in p or '*' in p:
                        print(f'[HIGH] Overly broad trust: {role[\"RoleName\"]} -> {p}')
"

IAM 提权路径检测:

攻击手法MITRE ATT&CK检测要点取证日志源
委托角色(AssumeRole)T1078.004检查异常的 AssumeRole 调用CloudTrail AssumeRole 事件
策略版本修改T1098.003监控 CreatePolicyVersionSetDefaultPolicyVersionCloudTrail IAM 事件
用户权限边界绕过T1078.004检查 PutUserPermissionsBoundary 调用CloudTrail IAM 事件
服务账户令牌创建T1078.004监控 CreateAccessKey 对服务账户的操作CloudTrail IAM 事件
资源策略修改T1098.003监控 S3 存储桶策略、SQS 策略变更CloudTrail 配置事件

Azure AD 配置审计

Azure AD(现 Entra ID)的权限模型包含目录角色、应用程序权限和委托权限三个层次。

目录角色过度分配检测:

az rest --method GET \
  --url "https://graph.microsoft.com/v1.0/directoryRoleAssignments" \
  --query "value[?contains(roleId, 'e8611ab8-c189-46e8-94e1-60193e7e4f99') || contains(roleId, 'f28a1ee5-3d28-4e93-88f5-53b03e48f73e')].{Principal:principalId,Role:roleId}" \
  --output table

Azure AD 权限提升检测要点:

目录角色权限等级风险描述检测日志
Global Administrator🔴 最高完全控制 Azure AD 所有设置AuditLogs
Privileged Role Administrator🔴 高可分配任何目录角色AuditLogs
Application Administrator🟡 中可创建和管理应用程序AuditLogs
User Administrator🟡 中可重置用户密码AuditLogs
Security Administrator🟡 中可管理安全策略和警报AuditLogs

Azure AD 检测命令:

az rest --method GET \
  --url "https://graph.microsoft.com/v1.0/auditLogs/directories" \
  --query "value[?contains(category, 'RoleManagement')].{Time:activityDateTime,Operation:operationName,Actor:initiatedBy.user.userPrincipalName,Target:targetResources[0].displayName}" \
  --output table

GCP IAM 审计

GCP IAM 采用资源层次结构,权限从 Organization 向下继承。

GCP IAM 异常权限检测:

gcloud asset search-all-iam-policies \
  --scope=organizations/123456789 \
  --query="policy:roles/owner OR policy:roles/editor" \
  --format="table(policy.bindings[].members, policy.bindings[].role, resource)" \
  --flatten="policy.bindings[]"

Cloud IAM 权限边界对比:

云平台最小权限实现权限边界机制跨账户风险
AWSIAM Policy + SCPPermission Boundary + SCP跨账户角色信任
AzureRBAC 角色分配条件访问策略租户间应用隔离
GCPIAM Policy BindingOrganization Policy服务账户密钥泄露
GCPResource HierarchyVPC Service Controls跨项目权限继承

0x03 OAuth/OIDC 协议攻击与令牌劫持取证

OAuth 2.0 授权码注入攻击

授权码注入(Authorization Code Injection, MITRE ATT&CK T1550.001)是 OAuth 协议最常见的攻击方式之一。攻击者通过操纵授权码重定向将受害者的认证会话绑定到攻击者控制的账户。

攻击流程:

  1. 攻击者向受害者发送恶意链接,其中包含攻击者的 state 参数
  2. 受害者点击链接,完成身份认证
  3. 授权服务器将授权码重定向到攻击者控制的回调地址
  4. 攻击者使用受害者的授权码获取令牌

检测授权码注入的 Python 脚本:

import json
import re
import sys
from collections import defaultdict
from datetime import datetime, timedelta

def analyze_oauth_redirect_logs(log_file):
    redirects = defaultdict(list)
    anomalies = []
    
    with open(log_file, 'r') as f:
        for line in f:
            try:
                entry = json.loads(line)
            except json.JSONDecodeError:
                continue
            
            if entry.get('event_type') not in ('oauth_redirect', 'authorization_code_grant'):
                continue
            
            client_id = entry.get('client_id', 'unknown')
            redirect_uri = entry.get('redirect_uri', 'unknown')
            user_agent = entry.get('user_agent', 'unknown')
            source_ip = entry.get('source_ip', 'unknown')
            timestamp = entry.get('timestamp', '')
            state_param = entry.get('state', '')
            session_id = entry.get('session_id', '')
            
            key = f"{client_id}:{session_id}"
            redirects[key].append({
                'timestamp': timestamp,
                'redirect_uri': redirect_uri,
                'source_ip': source_ip,
                'user_agent': user_agent,
                'state': state_param
            })
    
    for session_key, events in redirects.items():
        if len(events) < 2:
            continue
        
        uris = set(e['redirect_uri'] for e in events)
        ips = set(e['source_ip'] for e in events)
        agents = set(e['user_agent'] for e in events)
        states = set(e['state'] for e in events)
        
        if len(uris) > 1:
            anomalies.append({
                'type': 'REDIRECT_URI_VARIANCE',
                'severity': 'HIGH',
                'session': session_key,
                'detail': f'Multiple redirect URIs: {uris}',
                'events': events
            })
        
        if len(states) > 1 and len(ips) > 1:
            anomalies.append({
                'type': 'STATE_PARAM_REUSE',
                'severity': 'CRITICAL',
                'session': session_key,
                'detail': f'State reuse across IPs: {ips}',
                'events': events
            })
        
        if len(agents) > 1:
            anomalies.append({
                'type': 'USER_AGENT_VARIANCE',
                'severity': 'MEDIUM',
                'session': session_key,
                'detail': f'UA change within session: {agents}',
                'events': events
            })
        
        timestamps = []
        for e in events:
            try:
                ts = datetime.fromisoformat(e['timestamp'].replace('Z', '+00:00'))
                timestamps.append(ts)
            except (ValueError, AttributeError):
                pass
        
        if len(timestamps) >= 2:
            timestamps.sort()
            time_span = (timestamps[-1] - timestamps[0]).total_seconds()
            if time_span < 10 and len(ips) > 1:
                anomalies.append({
                    'type': 'RAPID_IP_CHANGE',
                    'severity': 'CRITICAL',
                    'session': session_key,
                    'detail': f'IP changed within {time_span:.1f}s: {ips}',
                    'events': events
                })
    
    return anomalies

def print_report(anomalies):
    severity_order = {'CRITICAL': 0, 'HIGH': 1, 'MEDIUM': 2, 'LOW': 3}
    anomalies.sort(key=lambda x: severity_order.get(x['severity'], 99))
    
    print(f'OAuth Anomaly Report - {datetime.now().isoformat()}')
    print(f'Total anomalies found: {len(anomalies)}')
    print('=' * 70)
    
    for i, anomaly in enumerate(anomalies, 1):
        print(f'\n[{anomaly["severity"]}] #{i} - {anomaly["type"]}')
        print(f'Session: {anomaly["session"]}')
        print(f'Detail: {anomaly["detail"]}')
        print(f'Related events: {len(anomaly["events"])}')
        print('-' * 50)

if __name__ == '__main__':
    if len(sys.argv) < 2:
        print(f'Usage: {sys.argv[0]} <oauth_log_file>')
        sys.exit(1)
    
    results = analyze_oauth_redirect_logs(sys.argv[1])
    print_report(results)

Token 窃取与滥用检测

OAuth 令牌是身份认证的核心凭据,一旦被窃取,攻击者可以完全冒充受害者。

令牌滥用检测 Sigma 规则:

title: OAuth Token Replay from Unusual Location
id: 5a2d3e4f-6b7c-8d9e-0f1a-2b3c4d5e6f7a
status: experimental
description: Detects potential OAuth token replay attacks based on geographic impossibility
references:
  - https://attack.mitre.org/techniques/T1550/001/
author: BlueTeam Forensics
date: 2026/07/18
tags:
  - attack.credential_access
  - attack.t1550.001
  - attack.t1078
logsource:
  category: authentication
  service: oauth
detection:
  selection_event:
    event_id:
      - '53000'
      - '53001'
      - '53002'
  selection_token:
    token_type:
      - 'access_token'
      - 'refresh_token'
  condition: selection_event and selection_token
  timeframe: 1h
  fields:
    - source_ip
    - user_agent
    - token_issuer
    - client_id
    - geographic_location
  filters:
    geo_impossible:
      field: geographic_distance_km
      operator: '>'
      value: 800
    time_impossible:
      field: time_between_requests_sec
      operator: '<'
      value: 300
falsepositives:
  - VPN reconnection
  - CDN edge node routing
level: high

Redirect URI 操纵与 PKCE 绕过

常见 Redirect URI 操纵手法对比:

操纵手法描述危害等级检测方法
子域名接管注册目标域名的已过期子域名🔴 高DNS 记录审计 + 证书透明度日志
路径遍历利用 URL 解析差异注入回调路径🔴 高严格字符串匹配审计
开放重定向利用通过应用层重定向绕过白名单🟡 中重定向链分析
Fragment 操纵利用 fragment 不发送到服务器的特性🟡 中客户端日志审计
HTTP 参数污染利用框架解析差异注入额外参数🟡 中请求参数规范化检查

PKCE 绕过检测:

jwt_tool() {
    local token="$1"
    echo "$token" | cut -d'.' -f2 | base64 -d 2>/dev/null | python3 -c "
import json, sys
try:
    payload = json.load(sys.stdin)
    print(f'Header: {json.dumps(header, indent=2)}')
    print(f'Payload: {json.dumps(payload, indent=2)}')
    alg = payload.get('header', {}).get('alg', payload.get('alg', 'unknown'))
    if alg in ('none', 'HS256'):
        print('[WARNING] Weak algorithm detected')
    if 'pkce' not in str(payload).lower():
        print('[INFO] No PKCE claim in token')
except Exception as e:
    print(f'Parse error: {e}')
"
}

0x04 MFA 绕过与会话劫持取证

SIM Swap 与 SS7 攻击

SIM Swap 攻击是绕过 SMS-based MFA 的最常见方式。攻击者通过社会工程或内部人员协助,将受害者的手机号转移到攻击者控制的 SIM 卡上。

SIM Swap 检测特征:

检测维度正常基线异常指标置信度
SIM 变更频率< 1次/年短时间内多次变更🔴 高
变更渠道运营商门店客服电话/在线🟡 中
变更后登录与 SIM 变更时间一致变更后立即出现新设备登录🔴 高
MFA 重试模式偶尔失败短时间内大量失败后成功🔴 高

SS7 攻击检测日志分析:

grep -E "(sms_receive|sms_forward|ss7_route|location_update)" /var/log/sms_gateway.log | \
python3 -c "
import sys
from collections import Counter
numbers = []
for line in sys.stdin:
    parts = line.strip().split('|')
    if len(parts) >= 4:
        action = parts[2]
        number = parts[3]
        numbers.append((action, number))
        
action_counts = Counter(a for a, n in numbers)
for action, count in action_counts.most_common(20):
    print(f'{action}: {count}')
"

MFA 疲劳攻击(MFA Fatigue)

MFA 疲劳攻击(MITRE ATT&CK T1621)通过反复发送 MFA 推送通知直到用户误批准。Okta、Microsoft Authenticator 等平台都曾遭受此类攻击。

MFA 疲劳攻击检测 Sigma 规则:

title: MFA Fatigue Attack Detection
id: 7c8d9e0f-1a2b-3c4d-5e6f-7a8b9c0d1e2f
status: stable
description: Detects potential MFA fatigue attacks through repeated push notification approvals
references:
  - https://attack.mitre.org/techniques/T1621/
author: BlueTeam Forensics
date: 2026/07/18
tags:
  - attack.initial_access
  - attack.t1621
  - attack.credential_access
logsource:
  category: authentication
  service: mfa
detection:
  selection_push:
    auth_method:
      - 'push_notification'
      - 'mfa_push'
      - 'totp'
  selection_failed:
    auth_result:
      - 'denied'
      - 'timeout'
  selection_success:
    auth_result:
      - 'approved'
      - 'success'
  condition: selection_push and (selection_failed R10 selection_success)
  timeframe: 10m
  count_threshold: 5
  fields:
    - user_id
    - source_ip
    - device_id
    - user_agent
    - notification_timestamp
    - approval_timestamp
  extra:
    time_diff:
      field: approval_timestamp
      minus: notification_timestamp
      threshold_seconds: 60
falsepositives:
  - User legitimately declined and re-approved
  - Network latency causing delayed notifications
level: critical

Python MFA 异常行为检测脚本:

import json
import sys
from collections import defaultdict
from datetime import datetime, timedelta

class MFADetector:
    def __init__(self):
        self.events = defaultdict(list)
        self.alerts = []
    
    def load_events(self, log_file):
        with open(log_file, 'r') as f:
            for line in f:
                try:
                    entry = json.loads(line)
                except json.JSONDecodeError:
                    continue
                
                if entry.get('category') == 'mfa':
                    user = entry.get('user_id', 'unknown')
                    self.events[user].append({
                        'timestamp': entry.get('timestamp'),
                        'method': entry.get('auth_method'),
                        'result': entry.get('auth_result'),
                        'source_ip': entry.get('source_ip'),
                        'device_id': entry.get('device_id'),
                        'user_agent': entry.get('user_agent', '')
                    })
    
    def detect_fatigue_attack(self, window_minutes=10, threshold=5):
        for user, events in self.events.items():
            events.sort(key=lambda x: x.get('timestamp', ''))
            
            for i, event in enumerate(events):
                if event.get('result') not in ('approved', 'success'):
                    continue
                
                window_start = datetime.fromisoformat(
                    event['timestamp'].replace('Z', '+00:00')
                ) - timedelta(minutes=window_minutes)
                
                recent_denials = []
                for j in range(max(0, i - 20), i):
                    prev = events[j]
                    try:
                        prev_ts = datetime.fromisoformat(
                            prev['timestamp'].replace('Z', '+00:00')
                        )
                    except (ValueError, KeyError):
                        continue
                    
                    if prev_ts >= window_start and prev.get('result') in ('denied', 'timeout'):
                        recent_denials.append(prev)
                
                if len(recent_denials) >= threshold:
                    self.alerts.append({
                        'type': 'MFA_FATIGUE',
                        'severity': 'CRITICAL',
                        'user': user,
                        'denial_count': len(recent_denials),
                        'approval_event': event,
                        'window_start': window_start.isoformat(),
                        'mitre': 'T1621'
                    })
    
    def detect_impossible_travel(self, max_speed_kmh=900):
        for user, events in self.events.items():
            for i in range(1, len(events)):
                curr = events[i]
                prev = events[i - 1]
                
                try:
                    curr_ts = datetime.fromisoformat(
                        curr['timestamp'].replace('Z', '+00:00')
                    )
                    prev_ts = datetime.fromisoformat(
                        prev['timestamp'].replace('Z', '+00:00')
                    )
                except (ValueError, KeyError):
                    continue
                
                time_diff = (curr_ts - prev_ts).total_seconds()
                if time_diff <= 0 or time_diff > 86400:
                    continue
                
                if curr.get('source_ip') != prev.get('source_ip'):
                    self.alerts.append({
                        'type': 'MFA_GEO_ANOMALY',
                        'severity': 'HIGH',
                        'user': user,
                        'prev_ip': prev.get('source_ip'),
                        'curr_ip': curr.get('source_ip'),
                        'time_diff_seconds': time_diff,
                        'mitre': 'T1078'
                    })
    
    def detect_device_anomaly(self):
        user_devices = defaultdict(lambda: defaultdict(int))
        user_agents = defaultdict(lambda: defaultdict(int))
        
        for user, events in self.events.items():
            for event in events:
                if event.get('result') in ('approved', 'success'):
                    device = event.get('device_id', 'unknown')
                    ua = event.get('user_agent', 'unknown')
                    user_devices[user][device] += 1
                    user_agents[user][ua] += 1
        
        for user, devices in user_devices.items():
            if len(devices) > 3:
                self.alerts.append({
                    'type': 'MULTI_DEVICE_MFA',
                    'severity': 'MEDIUM',
                    'user': user,
                    'device_count': len(devices),
                    'devices': dict(devices),
                    'mitre': 'T1078'
                })
    
    def generate_report(self):
        severity_order = {'CRITICAL': 0, 'HIGH': 1, 'MEDIUM': 2, 'LOW': 3}
        self.alerts.sort(key=lambda x: severity_order.get(x['severity'], 99))
        
        print(f'MFA Security Analysis Report')
        print(f'Generated: {datetime.now().isoformat()}')
        print(f'Total users analyzed: {len(self.events)}')
        print(f'Total alerts: {len(self.alerts)}')
        print('=' * 70)
        
        for i, alert in enumerate(self.alerts, 1):
            print(f'\n[{alert["severity"]}] Alert #{i}: {alert["type"]}')
            print(f'  User: {alert["user"]}')
            print(f'  MITRE ATT&CK: {alert["mitre"]}')
            for k, v in alert.items():
                if k not in ('type', 'severity', 'user', 'mitre'):
                    print(f'  {k}: {v}')
            print('-' * 50)

if __name__ == '__main__':
    if len(sys.argv) < 2:
        print(f'Usage: {sys.argv[0]} <mfa_log_file>')
        sys.exit(1)
    
    detector = MFADetector()
    detector.load_events(sys.argv[1])
    detector.detect_fatigue_attack()
    detector.detect_impossible_travel()
    detector.detect_device_anomaly()
    detector.generate_report()

会话劫持攻击对比:

攻击手法MITRE ATT&CK攻击原理检测方法
Session FixationT1539强制使用已知 Session IDSession ID 变更审计
Cookie 伪造T1550.004直接构造有效 Session CookieCookie 签名验证日志
会话令牌窃取T1539从内存/日志/网络中提取令牌异常 Token 使用模式
会话重放T1550.004重放已截获的会话令牌令牌一次性使用检查

0x05 特权访问管理(PAM)攻防取证

密码保险箱攻击

特权访问管理(Privileged Access Management, PAM)系统存储了企业最敏感的凭据。攻击者一旦攻破 PAM 系统,即可获取全域特权访问权限。

密码保险箱攻击向量:

攻击向量MITRE ATT&CK攻击描述取证重点
API 令牌窃取T1552.001窃取 PAM 系统的 API 访问令牌API 调用日志、令牌生命周期
管理员后门T1556在 PAM 系统中植入后门账户管理员账户变更日志
凭据导出T1552.001批量导出所有存储的凭据批量读取操作审计
复制粘贴窃取T1552.001通过剪贴板获取检索的凭据终端 DLP 日志
JIT 提权滥用T1078.004滥用 Just-In-Time 提权机制JIT 审批和使用日志

PAM 日志分析脚本:

#!/bin/bash
PAM_LOG="${1:-/var/log/pam-audit.log}"
REPORT_FILE="/tmp/pam_audit_report_$(date +%Y%m%d_%H%M%S).txt"

echo "PAM Security Audit Report" > "$REPORT_FILE"
echo "Generated: $(date)" >> "$REPORT_FILE"
echo "Log Source: $PAM_LOG" >> "$REPORT_FILE"
echo "========================================" >> "$REPORT_FILE"

echo "" >> "$REPORT_FILE"
echo "[1] Failed Authentication Attempts" >> "$REPORT_FILE"
grep -c "authentication_failure\|auth_fail\|login_failed" "$PAM_LOG" >> "$REPORT_FILE"
echo "" >> "$REPORT_FILE"
grep "authentication_failure\|auth_fail\|login_failed" "$PAM_LOG" | \
  awk '{print $1, $2, $3, $5}' | sort | uniq -c | sort -rn | head -20 >> "$REPORT_FILE"

echo "" >> "$REPORT_FILE"
echo "[2] Privilege Escalation Events" >> "$REPORT_FILE"
grep -i "sudo\|su_exec\|runas\|elevate" "$PAM_LOG" | \
  awk '{print $1, $2, $3, $5}' | sort | uniq -c | sort -rn | head -20 >> "$REPORT_FILE"

echo "" >> "$REPORT_FILE"
echo "[3] Credential Retrieval Operations" >> "$REPORT_FILE"
grep -i "retrieve\|checkout\|read_password\|get_credential" "$PAM_LOG" | \
  awk '{print $1, $2, $3, $5}' | sort | uniq -c | sort -rn | head -20 >> "$REPORT_FILE"

echo "" >> "$REPORT_FILE"
echo "[4] After-Hours Access (18:00-08:00)" >> "$REPORT_FILE"
grep -E "2[0-3]:|0[0-7]:" "$PAM_LOG" | grep -i "login\|access\|auth" | \
  awk '{print $1, $2, $3, $5}' | sort | uniq -c | sort -rn | head -20 >> "$REPORT_FILE"

echo "" >> "$REPORT_FILE"
echo "[5] Admin Account Usage" >> "$REPORT_FILE"
grep -iE "admin|root|superuser|privileged" "$PAM_LOG" | \
  awk '{print $1, $2, $3, $5}' | sort | uniq -c | sort -rn | head -20 >> "$REPORT_FILE"

echo "" >> "$REPORT_FILE"
echo "[6] Concurrent Session Anomalies" >> "$REPORT_FILE"
grep -i "session_start\|session_open" "$PAM_LOG" | \
  awk '{print $1, $2, $3, $5}' | sort | uniq -c | sort -rn | \
  awk '$1 > 3 {print "[ALERT] Multiple concurrent sessions:", $0}' >> "$REPORT_FILE"

echo "" >> "$REPORT_FILE"
echo "[7] Bulk Credential Access" >> "$REPORT_FILE"
grep -i "retrieve\|checkout\|read_password" "$PAM_LOG" | \
  awk '{print $5}' | sort | uniq -c | sort -rn | \
  awk '$1 > 10 {print "[ALERT] Excessive credential access by:", $2, "- Count:", $1}' >> "$REPORT_FILE"

echo "" >> "$REPORT_FILE"
echo "Report saved to: $REPORT_FILE"
cat "$REPORT_FILE"

JIT 提权滥用分析

Just-In-Time(JIT)提权是 PAM 系统的最佳实践之一,通过临时授予特权来减少永久特权暴露。然而,攻击者可以滥用 JIT 机制实现隐蔽的特权提升。

JIT 滥用检测 Sigma 规则:

title: JIT Privilege Escalation Abuse Detection
id: 3f4a5b6c-7d8e-9f0a-1b2c-3d4e5f6a7b8c
status: experimental
description: Detects potential abuse of Just-In-Time privilege elevation mechanisms
references:
  - https://attack.mitre.org/techniques/T1078/004/
author: BlueTeam Forensics
date: 2026/07/18
tags:
  - attack.privilege_escalation
  - attack.t1078.004
logsource:
  category: pam
  service: jit
detection:
  selection_jit:
    event_type:
      - 'jit_access_request'
      - 'jit_access_granted'
      - 'jit_elevation'
  selection_anomaly:
    request_outside_business_hours: true
    no_approval: true
    duration_exceeds_normal: true
  condition: selection_jit and selection_anomaly
  fields:
    - user_id
    - requested_role
    - approval_status
    - duration_minutes
    - request_timestamp
    - activation_timestamp
    - source_ip
    - justification
  timeframe: 24h
  aggregation:
    field: user_id
    function: count
    operator: '>'
    value: 3
falsepositives:
  - Emergency on-call access
  - Planned maintenance window
level: high

特权会话监控分析

特权会话是 PAM 取证的关键证据来源。通过分析特权会话的录制和审计日志,可以还原攻击者的操作轨迹。

监控维度正常基线异常检测规则证据价值
会话时长< 30 分钟超长会话(> 2小时)🟡 中
操作频率10-50 次/会话异常高频操作(> 200次)🟡 中
文件访问范围预期工作目录跨目录/跨系统访问🔴 高
数据传输正常工作流量大量数据下载/外传🔴 高
时间窗口工作时间非工作时间操作🟡 中
操作一致性操作与申请理由匹配操作与申请理由不匹配🔴 高

0x06 身份联邦与 SSO 安全取证

SAML 断言伪造

SAML(Security Assertion Markup Language)断言伪造是联邦身份系统最严重的攻击类型。攻击者通过伪造或篡改 SAML 断言,可以绕过认证机制获得未授权访问。

SAML 攻击类型对比:

攻击类型MITRE ATT&CK攻击原理防御措施
断言签名绕过T1550.003利用 XML 签名包装攻击(XML Signature Wrapping)严格签名验证
断言属性篡改T1550.003修改断言中的用户属性和角色声明断言加密 + 完整性检查
重放攻击T1550.003重放已截获的有效断言断言有效期检查 + OneTimeUse
密钥窃取T1552.004窃取 IdP 签名私钥HSM 密钥管理
元数据操纵T1550.003篡改 SP 或 IdP 元数据元数据签名验证

SAML 断言分析 Python 脚本:

import base64
import json
import sys
import xml.etree.ElementTree as ET
from datetime import datetime
from urllib.parse import unquote

def decode_saml_response(encoded_response):
    decoded = base64.b64decode(unquote(encoded_response))
    return decoded.decode('utf-8', errors='replace')

def parse_saml_assertion(xml_content):
    findings = []
    
    try:
        root = ET.fromstring(xml_content)
    except ET.ParseError as e:
        findings.append({
            'type': 'PARSE_ERROR',
            'severity': 'HIGH',
            'detail': f'SAML XML parse failure: {str(e)}'
        })
        return findings
    
    ns = {
        'saml': 'urn:oasis:names:tc:SAML:2.0:assertion',
        'samlp': 'urn:oasis:names:tc:SAML:2.0:protocol',
        'ds': 'http://www.w3.org/2000/09/xmldsig#'
    }
    
    assertions = root.findall('.//saml:Assertion', ns)
    for assertion in assertions:
        issue_instant = assertion.get('IssueInstant', '')
        if issue_instant:
            try:
                issue_dt = datetime.fromisoformat(
                    issue_instant.replace('Z', '+00:00')
                )
                now = datetime.now(issue_dt.tzinfo)
                age = (now - issue_dt).total_seconds()
                if age > 300:
                    findings.append({
                        'type': 'STALE_ASSERTION',
                        'severity': 'MEDIUM',
                        'detail': f'Assertion age: {age:.0f}s (threshold: 300s)',
                        'issue_instant': issue_instant
                    })
            except ValueError:
                pass
        
        subject = assertion.find('.//saml:Subject', ns)
        if subject is not None:
            name_id = subject.find('.//saml:NameID', ns)
            if name_id is not None:
                name_value = name_id.text or ''
                if 'admin' in name_value.lower() or 'root' in name_value.lower():
                    findings.append({
                        'type': 'ADMIN_ASSERTION',
                        'severity': 'INFO',
                        'detail': f'Admin subject: {name_value}',
                        'subject': name_value
                    })
        
        conditions = assertion.find('.//saml:Conditions', ns)
        if conditions is not None:
            not_before = conditions.get('NotBefore', '')
            not_on_or_after = conditions.get('NotOnOrAfter', '')
            
            if not_before and not_on_or_after:
                try:
                    nb = datetime.fromisoformat(not_before.replace('Z', '+00:00'))
                    noa = datetime.fromisoformat(not_on_or_after.replace('Z', '+00:00'))
                    validity = (noa - nb).total_seconds()
                    if validity > 3600:
                        findings.append({
                            'type': 'OVERLY_LONG_VALIDITY',
                            'severity': 'MEDIUM',
                            'detail': f'Assertion validity window: {validity:.0f}s (recommended: < 300s)'
                        })
                except ValueError:
                    pass
        
        authn_statement = assertion.find('.//saml:AuthnStatement', ns)
        if authn_statement is not None:
            authn_context = authn_statement.find('.//saml:AuthnContext', ns)
            if authn_context is not None:
                authn_class = authn_context.find('.//saml:AuthnContextClassRef', ns)
                if authn_class is not None and authn_class.text:
                    ctx = authn_class.text
                    if 'unspecified' in ctx or 'none' in ctx.lower():
                        findings.append({
                            'type': 'WEAK_AUTHN_CONTEXT',
                            'severity': 'HIGH',
                            'detail': f'Weak authentication context: {ctx}'
                        })
        
        signature = assertion.find('.//ds:Signature', ns)
        if signature is None:
            findings.append({
                'type': 'UNSIGNED_ASSERTION',
                'severity': 'CRITICAL',
                'detail': 'Assertion is not signed'
            })
        
        attributes = assertion.find('.//saml:AttributeStatement', ns)
        if attributes is not None:
            for attr in attributes.findall('saml:Attribute', ns):
                attr_name = attr.get('Name', '')
                for val in attr.findall('saml:AttributeValue', ns):
                    value = val.text or ''
                    if 'role' in attr_name.lower() and 'admin' in value.lower():
                        findings.append({
                            'type': 'ADMIN_ROLE_ATTRIBUTE',
                            'severity': 'HIGH',
                            'detail': f'Admin role in attribute: {attr_name}={value}'
                        })
    
    return findings

def analyze_saml_request(request_data):
    findings = []
    
    if 'SAMLRequest' in request_data:
        encoded = request_data.split('SAMLRequest=')[1].split('&')[0]
        try:
            decoded = base64.b64decode(unquote(encoded))
            root = ET.fromstring(decoded)
            assertion_consumer_url = root.get('AssertionConsumerServiceURL', '')
            if assertion_consumer_url:
                findings.append({
                    'type': 'ACS_URL_INFO',
                    'severity': 'INFO',
                    'detail': f'ACS URL: {assertion_consumer_url}'
                })
        except Exception as e:
            findings.append({
                'type': 'REQUEST_PARSE_ERROR',
                'severity': 'MEDIUM',
                'detail': f'SAML Request parse issue: {str(e)}'
            })
    
    if 'SAMLResponse' in request_data:
        encoded = request_data.split('SAMLResponse=')[1].split('&')[0]
        xml_content = decode_saml_response(encoded)
        findings.extend(parse_saml_assertion(xml_content))
    
    return findings

def print_saml_report(findings):
    severity_order = {'CRITICAL': 0, 'HIGH': 1, 'MEDIUM': 2, 'LOW': 3, 'INFO': 4}
    findings.sort(key=lambda x: severity_order.get(x['severity'], 99))
    
    print('SAML Security Analysis Report')
    print('=' * 60)
    
    for i, f in enumerate(findings, 1):
        print(f'[{f["severity"]}] #{i} - {f["type"]}')
        print(f'  Detail: {f["detail"]}')
        print()

if __name__ == '__main__':
    if len(sys.argv) < 2:
        print(f'Usage: {sys.argv[0]} <saml_post_data_file>')
        sys.exit(1)
    
    with open(sys.argv[1], 'r') as f:
        data = f.read().strip()
    
    if data.startswith('{'):
        request_data = json.loads(data)
    else:
        request_data = {}
        for pair in data.split('&'):
            if '=' in pair:
                key, val = pair.split('=', 1)
                request_data[key] = unquote(val)
    
    results = analyze_saml_request(request_data)
    print_saml_report(results)

Kerberos 降级与身份传递攻击

攻击手法MITRE ATT&CK攻击原理检测方法
KerberoastingT1558.003离线破解服务账户 TGS 票据异常 TGS 请求量监控
AS-REP RoastingT1558.004攻击无预认证用户AS-REQ 日志异常检测
Golden TicketT1558.001伪造 TGT 票据异常 TGT 签名验证
Silver TicketT1558.002伪造 TGS 票据服务端票据验证日志
Pass-the-HashT1550.002使用 NTLM 哈希直接认证NTLM 认证异常模式
DCSyncT1003.006模拟域控复制凭据目录复制请求监控

Kerberos 攻击检测 Sigma 规则:

title: Kerberos Golden Ticket Detection
id: 9a0b1c2d-3e4f-5a6b-7c8d-9e0f1a2b3c4d
status: stable
description: Detects potential Golden Ticket attacks through abnormal Kerberos TGT patterns
references:
  - https://attack.mitre.org/techniques/T1558/001/
author: BlueTeam Forensics
date: 2026/07/18
tags:
  - attack.credential_access
  - attack.t1558.001
logsource:
  category: authentication
  service: kerberos
detection:
  selection_tgt:
    event_id:
      - '4768'
      - '4769'
    ticket_type:
      - 'TGT'
      - 'TGS'
  selection_anomaly:
    encryption_type:
      - '0x17'
      - '0x18'
    pre_auth_result: '0x0'
    service_name: 'krbtgt'
  condition: selection_tgt and selection_anomaly
  fields:
    - account_name
    - source_ip
    - ticket_encryption_type
    - ticket_options
    - target_service
    - logon_id
  timeframe: 1h
  aggregation:
    field: account_name
    function: count
    operator: '>'
    value: 100
falsepositives:
  - Normal TGT renewal cycle
  - Large user base concurrent authentication
level: critical

联邦信任链审计

SAML 信任关系映射:

az rest --method GET \
  --url "https://login.microsoftonline.com/{tenant_id}/federationSettings/DomainDns" \
  --query "value[?authenticationProtocol=='saml'].{Domain:name,Protocol:authenticationProtocol,SigningCert:signingCertificate}" \
  --output table

身份联邦安全审计检查清单:

审计项目检查内容风险等级检查方法
签名算法使用 SHA-1 签名🔴 高解析 IdP 元数据中的签名算法
证书过期IdP/SP 证书是否过期🟡 中检查 X.509 证书有效期
信任方向双向信任配置🔴 高审查 SP 和 IdP 元数据
断言加密断言是否加密传输🟡 中检查 SP 配置中的加密设置
URL 验证ACS URL 和 SLO URL 白名单🔴 高审查 SP 元数据中的 URL 配置

0x07 证据强度分层与案例关联

证据分级标准

在身份安全事件取证中,不同来源的证据具有不同的可信度和证明力。建立标准化的证据分级框架对于准确还原攻击链至关重要。

证据等级标记证据类型证明力典型来源
一级证据🔴加密签名的审计日志最高,难以伪造CloudTrail 签名日志、HSM 审计日志
二级证据🟡系统级认证日志较高,可被具有系统权限的攻击者篡改Windows Security Event Log、Linux auth.log
三级证据🟢应用级访问日志中等,需要交叉验证Web 应用 Access Log、API Gateway Log
补充证据网络流量、DNS 日志辅助验证PCAP、Zeek 日志、DNS 查询日志

证据关联方法

跨域日志关联分析:

python3 << 'EOF'
import json
import sys
from datetime import datetime, timedelta

def correlate_identity_events(cloudtrail_log, ad_log, vpn_log, oauth_log):
    events = []
    
    with open(cloudtrail_log, 'r') as f:
        for line in f:
            try:
                entry = json.loads(line)
                events.append({
                    'source': 'CloudTrail',
                    'timestamp': entry.get('eventTime', ''),
                    'user': entry.get('userIdentity', {}).get('arn', ''),
                    'action': entry.get('eventName', ''),
                    'source_ip': entry.get('sourceIPAddress', ''),
                    'event_id': entry.get('eventID', ''),
                    'detail': json.dumps(entry.get('requestParameters', {}))
                })
            except json.JSONDecodeError:
                continue
    
    with open(ad_log, 'r') as f:
        for line in f:
            try:
                entry = json.loads(line)
                events.append({
                    'source': 'AD',
                    'timestamp': entry.get('timestamp', ''),
                    'user': entry.get('subjectUserName', ''),
                    'action': entry.get('eventID', ''),
                    'source_ip': entry.get('ipAddress', ''),
                    'event_id': str(entry.get('eventRecordId', '')),
                    'detail': entry.get('description', '')
                })
            except json.JSONDecodeError:
                continue
    
    with open(vpn_log, 'r') as f:
        for line in f:
            try:
                entry = json.loads(line)
                events.append({
                    'source': 'VPN',
                    'timestamp': entry.get('timestamp', ''),
                    'user': entry.get('username', ''),
                    'action': entry.get('event_type', ''),
                    'source_ip': entry.get('public_ip', ''),
                    'event_id': entry.get('session_id', ''),
                    'detail': entry.get('message', '')
                })
            except json.JSONDecodeError:
                continue
    
    events.sort(key=lambda x: x.get('timestamp', ''))
    
    correlations = []
    for i, event in enumerate(events):
        if not event.get('source_ip'):
            continue
        
        window_start = datetime.fromisoformat(
            event['timestamp'].replace('Z', '+00:00')
        ) - timedelta(minutes=5)
        window_end = datetime.fromisoformat(
            event['timestamp'].replace('Z', '+00:00')
        ) + timedelta(minutes=5)
        
        related = []
        for j, other in enumerate(events):
            if i == j:
                continue
            try:
                other_ts = datetime.fromisoformat(
                    other['timestamp'].replace('Z', '+00:00')
                )
                if window_start <= other_ts <= window_end:
                    if (other['user'] == event['user'] or 
                        other['source_ip'] == event['source_ip']):
                        related.append({
                            'source': other['source'],
                            'timestamp': other['timestamp'],
                            'action': other['action'],
                            'match_reason': 'user' if other['user'] == event['user'] else 'ip'
                        })
            except (ValueError, KeyError):
                continue
        
        if len(related) >= 2:
            sources = set(r['source'] for r in related) | {event['source']}
            if len(sources) >= 2:
                correlations.append({
                    'primary_event': event,
                    'correlated_events': related,
                    'correlation_strength': len(sources),
                    'cross_source': list(sources)
                })
    
    return correlations

def print_correlation_report(correlations):
    print(f'Identity Event Correlation Report')
    print(f'Total correlated chains: {len(correlations)}')
    print('=' * 70)
    
    for i, corr in enumerate(correlations, 1):
        primary = corr['primary_event']
        print(f'\nCorrelation Chain #{i} (Strength: {corr["correlation_strength"]})')
        print(f'Sources: {", ".join(corr["cross_source"])}')
        print(f'Primary: [{primary["source"]}] {primary["action"]} by {primary["user"]}')
        print(f'  Time: {primary["timestamp"]}')
        print(f'  IP: {primary["source_ip"]}')
        print(f'  Related events:')
        for evt in corr['correlated_events']:
            print(f'    [{evt["source"]}] {evt["action"]} ({evt["match_reason"]} match)')
            print(f'      Time: {evt["timestamp"]}')
        print('-' * 50)

if __name__ == '__main__':
    if len(sys.argv) < 5:
        print(f'Usage: {sys.argv[0]} <cloudtrail> <ad_log> <vpn_log> <oauth_log>')
        sys.exit(1)
    
    results = correlate_identity_events(sys.argv[1], sys.argv[2], sys.argv[3], sys.argv[4])
    print_correlation_report(results)
EOF

攻击链重建方法论

身份安全事件的攻击链通常包含以下阶段,每个阶段都会在不同的日志系统中留下痕迹。

攻击阶段日志源关键 IOC时间关联
初始访问VPN 日志、SSO 登录日志新 IP、新设备事件开始
凭据获取认证失败/成功日志MFA 变更、密码重置初始访问后 0-24h
权限提升IAM 策略变更日志新角色、新权限凭据获取后 0-48h
横向移动服务间认证日志异常服务调用权限提升后 0-72h
数据访问应用访问日志、API 调用日志批量数据读取横向移动后 0-96h
数据外传网络出口日志、DLP 告警大量数据传输数据访问后

0x08 自动化检测与狩猎

Sigma 检测规则

云 IAM 策略篡改检测规则:

title: Cloud IAM Policy Tampering Detection
id: b1c2d3e4-f5a6-b7c8-d9e0-f1a2b3c4d5e6
status: stable
description: Detects potential IAM policy tampering activities across cloud platforms
references:
  - https://attack.mitre.org/techniques/T1098/003/
  - https://attack.mitre.org/techniques/T1098/001/
author: BlueTeam Forensics
date: 2026/07/18
tags:
  - attack.persistence
  - attack.t1098.003
  - attack.t1098.001
  - attack.privilege_escalation
logsource:
  category: cloud_trail
  service: iam
detection:
  selection_aws:
    event_source: 'iam.amazonaws.com'
    event_name:
      - 'CreatePolicyVersion'
      - 'SetDefaultPolicyVersion'
      - 'AttachUserPolicy'
      - 'AttachRolePolicy'
      - 'AttachGroupPolicy'
      - 'PutUserPolicy'
      - 'PutRolePolicy'
      - 'PutGroupPolicy'
      - 'CreateAccessKey'
      - 'UpdateAssumeRolePolicy'
  selection_azure:
    category: 'RoleManagement'
    operationName:
      - 'Add member to role'
      - 'Add eligible member to role'
      - 'Update role assignment'
  selection_gcp:
    methodName:
      - 'SetIamPolicy'
      - 'TestIamPermissions'
      - 'AddAllowedPolicyBinding'
  condition: selection_aws or selection_azure or selection_gcp
  fields:
    - event_time
    - user_identity_arn
    - event_name
    - request_parameters
    - source_ip_address
    - user_agent
  timeframe: 1h
  filters:
    privileged_actions:
      event_name:
        - 'CreatePolicyVersion'
        - 'AttachUserPolicy'
        - 'AttachRolePolicy'
        - 'UpdateAssumeRolePolicy'
    privileged_roles:
      role_name:
        - 'AdministratorAccess'
        - 'PowerUserAccess'
        - 'OrganizationAccountAccessRole'
falsepositives:
  - Legitimate IAM policy updates during maintenance
  - Infrastructure-as-Code deployments
  - Automated CI/CD pipeline role management
level: critical

异常身份认证模式检测规则:

title: Abnormal Authentication Pattern Detection
id: c2d3e4f5-a6b7-c8d9-e0f1-a2b3c4d5e6f7
status: experimental
description: Detects abnormal authentication patterns indicative of credential compromise
references:
  - https://attack.mitre.org/techniques/T1078/
  - https://attack.mitre.org/techniques/T1110/
author: BlueTeam Forensics
date: 2026/07/18
tags:
  - attack.credential_access
  - attack.t1078
  - attack.t1110
logsource:
  category: authentication
  service: all
detection:
  selection_brute_force:
    auth_result:
      - 'failure'
      - 'failed'
    event_id:
      - '4625'
      - '4771'
      - '4776'
  selection_password_spray:
    auth_result:
      - 'failure'
      - 'failed'
  selection_impossible_travel:
    auth_result:
      - 'success'
      - 'approved'
    fields_required:
      - source_ip
      - geographic_location
      - user_agent
  condition: selection_brute_force | count by source_ip > 10 within 5m
    or selection_password_spray | count(distinct account_name) by source_ip > 5 within 15m
  timeframe: 15m
  fields:
    - account_name
    - source_ip
    - auth_method
    - auth_result
    - user_agent
    - geographic_location
    - failure_reason
falsepositives:
  - Service account automation
  - Credential rotation scripts
  - Legitimate pentesting activities
level: high

Bash 云 IAM 审计脚本

#!/bin/bash
echo "============================================"
echo "  Multi-Cloud IAM Security Audit Script"
echo "  Generated: $(date)"
echo "============================================"

check_aws_iam() {
    echo ""
    echo "[AWS IAM AUDIT]"
    echo "-------------------------"
    
    echo "[1] IAM Users without MFA:"
    aws iam list-users --query 'Users[?MFADevices==`[]`].{UserName:UserName,CreateDate:CreateDate}' --output table 2>/dev/null
    
    echo ""
    echo "[2] IAM Users with Console Access:"
    aws iam generate-credential-report 2>/dev/null
    sleep 2
    aws iam get-credential-report --query 'Content' --output text 2>/dev/null | \
      base64 -d 2>/dev/null | \
      awk -F',' 'NR==1 || ($4=="true" && $8=="true") {print $1, $4, $8, $9}'
    
    echo ""
    echo "[3] Overly Permissive Policies:"
    for policy_arn in $(aws iam list-policies --scope Local --query 'Policies[].[Arn]' --output text 2>/dev/null); do
        default_ver=$(aws iam get-policy --policy-arn "$policy_arn" --query 'Policy.DefaultVersionId' --output text 2>/dev/null)
        has_wildcard=$(aws iam get-policy-version --policy-arn "$policy_arn" --version-id "$default_ver" --query 'PolicyVersion.Document.Statement[?Action==`*`]' --output text 2>/dev/null)
        if [ -n "$has_wildcard" ]; then
            echo "[CRITICAL] Wildcard policy: $policy_arn"
        fi
    done
    
    echo ""
    echo "[4] IAM Roles with Cross-Account Trust:"
    aws iam list-roles --query 'Roles[].{RoleName:RoleName,Arn:Arn}' --output json 2>/dev/null | \
      python3 -c "
import json, sys
roles = json.load(sys.stdin)
for role in roles:
    arn = role['Arn']
    trust = aws iam get-role --role-name $(echo $arn | cut -d'/' -f2) 2>/dev/null
" 2>/dev/null || true
}

check_azure_ad() {
    echo ""
    echo "[Azure AD AUDIT]"
    echo "-------------------------"
    
    if command -v az &> /dev/null; then
        echo "[1] Global Admins:"
        az ad directory-role list --query "[?displayName=='Global Administrator'].assignments[].{User:principalDisplayName}" --output table 2>/dev/null
        
        echo ""
        echo "[2] Users with No MFA:"
        az rest --method GET \
          --url "https://graph.microsoft.com/v1.0/users?\$select=displayName,userPrincipalName,authenticationMethods" \
          --query 'value[].{Name:displayName,UPN:userPrincipalName}' \
          --output table 2>/dev/null
    else
        echo "[SKIP] Azure CLI not installed"
    fi
}

check_gcp_iam() {
    echo ""
    echo "[GCP IAM AUDIT]"
    echo "-------------------------"
    
    if command -v gcloud &> /dev/null; then
        echo "[1] Project IAM Bindings:"
        gcloud asset search-all-iam-policies \
          --scope=organizations/$(gcloud organizations list --format="value(name)" 2>/dev/null | head -1 | cut -d'/' -f2) \
          --query="policy.bindings.role:" \
          --flatten="policy.bindings[]" \
          --format="table(policy.bindings.role, policy.bindings.members, resource)" 2>/dev/null | \
          grep -i "roles/owner\|roles/editor\|roles/iam.securityAdmin"
        
        echo ""
        echo "[2] Service Accounts with Keys:"
        gcloud iam service-accounts list --format="table(email,disabled)" 2>/dev/null | \
          while read -r sa_email _; do
            key_count=$(gcloud iam service-accounts keys list --iam-account="$sa_email" --format="value(name)" 2>/dev/null | wc -l)
            if [ "$key_count" -gt 0 ]; then
                echo "[WARNING] SA $sa_email has $key_count keys"
            fi
        done
    else
        echo "[SKIP] gcloud CLI not installed"
    fi
}

check_aws_iam
check_azure_ad
check_gcp_iam

echo ""
echo "============================================"
echo "  Audit Complete"
echo "============================================"

Python 身份行为基线分析

import json
import sys
from collections import defaultdict
from datetime import datetime, timedelta
from statistics import mean, stdev

class IdentityBehaviorBaseline:
    def __init__(self):
        self.user_events = defaultdict(list)
        self.baselines = {}
        self.anomalies = []
    
    def load_cloudtrail(self, log_file):
        with open(log_file, 'r') as f:
            for line in f:
                try:
                    event = json.loads(line)
                except json.JSONDecodeError:
                    continue
                
                user = event.get('userIdentity', {}).get('arn', 'unknown')
                ts = event.get('eventTime', '')
                action = event.get('eventName', '')
                source_ip = event.get('sourceIPAddress', '')
                ua = event.get('userAgent', '')
                
                try:
                    dt = datetime.fromisoformat(ts.replace('Z', '+00:00'))
                except (ValueError, AttributeError):
                    continue
                
                self.user_events[user].append({
                    'timestamp': dt,
                    'action': action,
                    'source_ip': source_ip,
                    'user_agent': ua,
                    'hour': dt.hour,
                    'weekday': dt.weekday()
                })
    
    def build_baselines(self):
        for user, events in self.user_events.items():
            if len(events) < 10:
                continue
            
            hours = [e['hour'] for e in events]
            action_types = defaultdict(int)
            ip_set = defaultdict(int)
            ua_set = defaultdict(int)
            
            for e in events:
                action_types[e['action']] += 1
                ip_set[e['source_ip']] += 1
                ua_set[e['user_agent']] += 1
            
            most_common_actions = sorted(action_types.items(), key=lambda x: x[1], reverse=True)[:10]
            most_common_ips = sorted(ip_set.items(), key=lambda x: x[1], reverse=True)[:5]
            
            self.baselines[user] = {
                'total_events': len(events),
                'mean_hour': mean(hours) if hours else 0,
                'hour_std': stdev(hours) if len(hours) > 1 else 0,
                'common_actions': set(a for a, _ in most_common_actions),
                'common_ips': set(ip for ip, _ in most_common_ips),
                'unique_actions': len(action_types),
                'unique_ips': len(ip_set),
                'activity_days': len(set(e['timestamp'].date() for e in events)),
                'events_per_day': len(events) / max(1, len(set(e['timestamp'].date() for e in events)))
            }
    
    def detect_anomalies(self, current_events_file):
        self.build_baselines()
        
        with open(current_events_file, 'r') as f:
            for line in f:
                try:
                    event = json.loads(line)
                except json.JSONDecodeError:
                    continue
                
                user = event.get('userIdentity', {}).get('arn', 'unknown')
                action = event.get('eventName', '')
                source_ip = event.get('sourceIPAddress', '')
                ts = event.get('eventTime', '')
                
                if user not in self.baselines:
                    continue
                
                baseline = self.baselines[user]
                
                try:
                    dt = datetime.fromisoformat(ts.replace('Z', '+00:00'))
                    hour = dt.hour
                except (ValueError, AttributeError):
                    continue
                
                if action not in baseline['common_actions']:
                    self.anomalies.append({
                        'type': 'NOVEL_ACTION',
                        'severity': 'HIGH',
                        'user': user,
                        'action': action,
                        'source_ip': source_ip,
                        'timestamp': ts,
                        'mitre': 'T1078'
                    })
                
                if source_ip not in baseline['common_ips']:
                    self.anomalies.append({
                        'type': 'NOVEL_IP',
                        'severity': 'MEDIUM',
                        'user': user,
                        'source_ip': source_ip,
                        'timestamp': ts,
                        'mitre': 'T1078'
                    })
                
                if abs(hour - baseline['mean_hour']) > 4:
                    self.anomalies.append({
                        'type': 'OFF_HOURS_ACTIVITY',
                        'severity': 'MEDIUM',
                        'user': user,
                        'action': action,
                        'hour': hour,
                        'mean_hour': baseline['mean_hour'],
                        'timestamp': ts,
                        'mitre': 'T1078'
                    })
    
    def generate_hunting_report(self):
        severity_order = {'CRITICAL': 0, 'HIGH': 1, 'MEDIUM': 2, 'LOW': 3}
        self.anomalies.sort(key=lambda x: severity_order.get(x['severity'], 99))
        
        print(f'Identity Behavior Anomaly Report')
        print(f'Total anomalies: {len(self.anomalies)}')
        print('=' * 70)
        
        for i, a in enumerate(self.anomalies, 1):
            print(f'[{a["severity"]}] #{i} - {a["type"]}')
            print(f'  User: {a["user"]}')
            print(f'  Time: {a.get("timestamp", "N/A")}')
            print(f'  MITRE: {a.get("mitre", "N/A")}')
            print('-' * 50)

if __name__ == '__main__':
    if len(sys.argv) < 3:
        print(f'Usage: {sys.argv[0]} <baseline_cloudtrail> <current_cloudtrail>')
        sys.exit(1)
    
    analyzer = IdentityBehaviorBaseline()
    analyzer.load_cloudtrail(sys.argv[1])
    analyzer.detect_anomalies(sys.argv[2])
    analyzer.generate_hunting_report()

CloudTrail 日志分析 SQL 模式

使用 Amazon Athena 分析 CloudTrail 日志是身份行为取证的核心手段。以下 SQL 模式可用于常见的身份攻击检测场景。

检测异常 AssumeRole 调用:

SELECT
    useridentity.arn AS actor_arn,
    eventname AS action,
    sourceipaddress AS source_ip,
    eventtime AS event_time,
    requestparameters
FROM cloudtrail_logs
WHERE eventname = 'AssumeRole'
    AND useridentity.type = 'AssumedRole'
    AND datehour >= date_format(date_add('day', -7, current_date), '%Y%m%d%H')
ORDER BY eventtime DESC
LIMIT 500;

检测 IAM 策略变更:

SELECT
    useridentity.arn AS actor,
    eventname AS action,
    eventtime AS time,
    requestparameters,
    sourceipaddress AS ip
FROM cloudtrail_logs
WHERE eventname IN (
    'CreatePolicyVersion', 'SetDefaultPolicyVersion',
    'AttachUserPolicy', 'AttachRolePolicy',
    'PutUserPolicy', 'PutRolePolicy',
    'UpdateAssumeRolePolicy'
)
    AND useridentity.type != 'AWSService'
ORDER BY eventtime DESC;

身份异常行为检测聚合查询:

SELECT
    useridentity.arn AS user_arn,
    COUNT(DISTINCT eventname) AS unique_actions,
    COUNT(DISTINCT sourceipaddress) AS unique_ips,
    MIN(eventtime) AS first_seen,
    MAX(eventtime) AS last_seen,
    COUNT(*) AS total_events
FROM cloudtrail_logs
WHERE datehour >= date_format(date_add('day', -1, current_date), '%Y%m%d%H')
GROUP BY useridentity.arn
HAVING COUNT(DISTINCT sourceipaddress) > 5
    OR COUNT(DISTINCT eventname) > 50
ORDER BY unique_ips DESC, unique_actions DESC;

0x09 公开案例分析

案例一:SolarWinds SAML 令牌伪造攻击(2020)

SolarWinds 事件是近年来最严重的供应链攻击之一,也是身份系统被滥用的典型案例。攻击者在 SolarWinds Orion 软件中植入 SUNBURST 后门,通过供应链分发至约 18,000 个组织,最终约 100 个高价值目标被实际入侵。

攻击链描述:

阶段时间线攻击操作使用技术
初始投毒2019年10月入侵 SolarWinds 构建环境,植入 SUNBURST 代码T1195.002 软件供应链
供应链分发2020年2月-6月通过软件更新通道分发恶意版本T1195.002 软件供应链
横向侦察2020年3月起SUNBURST 回连 C2,枚举 Active Directory 环境T1087.002 域账户枚举
凭据窃取持续进行使用 KEEMBARGE 提取内存中的令牌和凭据T1003.001 LSASS 内存
SAML 伪造持续进行窃取 SAML 签名证书,伪造 SAML 令牌T1550.003 SAML 令牌伪造
持久化持续进行使用伪造的 SAML 令牌维持对云服务的访问T1078 有效账户

取证发现:

取证维度发现内容证据类型IOC
恶意 DLLSolarWinds.Orion.Core.BusinessLayer.dll 被篡改文件哈希SHA256: c15abaf51e78ca560b1395f1f85645f69af655310b9b5cc73d25b38bd81eee42
C2 域名avsvmcloud.com 及子域名(DNS 隧道)网络 IOC*.avsvmcloud.com
SAML 签名证书攻击者导出 SAML signing certificate 私钥身份 IOC异常 SAML 签名模式
邮箱规则创建隐藏邮箱规则转发所有邮件邮件规则 IOCInbox Rules: ForwardTo
OAuth 应用创建高权限 OAuth 应用维持访问身份 IOC异常应用注册
云持久化在 Azure AD 中创建凭据和应用云 IOC异常服务主体

经验教训:

  1. SAML 证书轮换是关键:一旦 SAML 签名证书可能泄露,必须立即轮换。SolarWinds 攻击者正是利用被窃取的 SAML 证书伪造令牌
  2. 供应链完整性验证不可或缺:软件构建和分发管道必须实施严格的完整性验证机制
  3. 云身份审计需要跨域关联:攻击者在本地 AD 和 Azure AD 之间切换操作,单一视图无法发现完整攻击链
  4. 异常令牌使用检测:即使使用合法凭据,异常的地理位置、时间模式和行为基线偏差也能暴露攻击者

案例二:Okta 供应链攻击与 Lapsus$ MFA 绕过(2022)

2022 年初,Lapsus$ 组织对 Okta 发起了大规模攻击,通过社会工程手段获取了 Okta 支持工程师的工作凭据,并利用远程管理工具对客户环境进行了未授权访问。

攻击链描述:

阶段时间线攻击操作使用技术
初始入侵2022年1月入侵第三方提供商 Sykes Enterprises 的 Okta 管理员T1133 外部远程服务
权限扩展2022年1月在 Okta 支持系统中获取 Super Admin 权限T1078.004 云账户
MFA 绕过2022年1月利用会话令牌重放绕过 MFA 检查T1621 MFA 疲劳攻击
客户访问2022年1-3月通过 Okta Admin Console 访问客户租户T1078 有效账户
数据窃取2022年3月对客户环境执行截图和数据外传T1005 数据从本地系统获取

取证发现:

取证维度发现内容证据类型IOC
异常登录支持工程师账户从异常 IP 登录认证日志异常 Source IP
操作时间大量操作发生在非工作时间时间线分析UTC 凌晨时段操作
截图操作执行大量屏幕截图操作终端日志特定进程创建
工具下载下载远程桌面和渗透测试工具文件日志ScreenConnect、AnyDesk
客户范围约 366 个客户租户被访问Okta 审计日志大量租户切换操作

经验教训:

  1. MFA 并非万能:社会工程和供应链攻击可以完全绕过 MFA 保护
  2. 第三方访问管理至关重要:供应商的特权访问必须实施最小权限和实时监控
  3. 会话令牌保护:Okta 管理控制台的会话令牌需要加强保护,防止重放攻击
  4. 异常行为检测:支持人员的大规模跨租户操作应该触发告警
  5. 零信任网络:即使通过 VPN 访问,也需要持续验证身份和设备状态

案例三:Azure AD Golden Ticket 攻击

Azure AD Kerberos 攻击利用了混合身份环境中 Active Directory 与 Azure AD 之间的信任关系。

攻击链描述:

阶段攻击操作技术编号
凭据获取通过 DCSync 获取 krbtgt NTLM 哈希T1003.006
Golden Ticket 创建使用 krbtgt 哈希伪造 TGT 票据T1558.001
域控访问使用伪造 TGT 访问域控制器T1078 有效账户
混合身份利用利用 Azure AD Connect 同步账户提权T1078.004 云账户
云服务访问通过同步账户访问 Azure ADT1550.003 SAML 令牌伪造

取证发现与检测方法:

检测维度正常模式攻击特征检测方法
TGT 生命周期正常 10h 有效期异常超长有效期事件 ID 4768/4769 分析
加密类型AES-256 (0x11)RC4 (0x17)加密类型筛选
事件频率单账户每小时 <10 次单账户高频请求聚合分析
登录事件标准 Windows 登录Kerberos 无 NTLM事件 ID 4624 类型分析

经验教训:

  1. 定期轮换 krbtgt 密码:至少每 180 天轮换两次(间隔 12 小时以上)
  2. 监控 DCSync 操作:非 DC 服务器发起的目录复制请求必须告警
  3. 混合身份安全审查:Azure AD Connect 同步账户的权限需要最小化
  4. Kerberos 加密策略:禁用 RC4 加密,强制使用 AES 加密

0x10 参考资料

编号标题类型URL
1AWS IAM Security Best Practices官方文档https://docs.aws.amazon.com/IAM/latest/UserGuide/best-practices.html
2Microsoft Entra ID Security Fundamentals官方文档https://learn.microsoft.com/en-us/entra/identity/enterprise-security/fundamentals-security-operations
3Google Cloud IAM Best Practices官方文档https://cloud.google.com/iam/docs/best-practices
4NIST SP 800-63B: Digital Identity Guidelines标准文档https://csrc.nist.gov/publications/detail/sp/800-63b/final
5OWASP Authentication Cheat Sheet安全指南https://cheatsheetseries.owasp.org/cheatsheets/Authentication_Cheat_Sheet.html
6Microsoft: SolarWinds Cyberattack Investigation案例分析https://msrc-blog.microsoft.com/2020/12/13/customer-guidance-on-recent-nation-state-cyber-attacks/
7Okta: Lapsus$ Investigation Report案例分析https://sec.okta.com/okta-lapsus-investigation-update
8MITRE ATT&CK: Identity and Access Management攻击框架https://attack.mitre.org/matrices/enterprise/credential-access/
9AWS CloudTrail User Guide官方文档https://docs.aws.amazon.com/awscloudtrail/latest/userguide/cloudtrail-user-guide.html
10SpecterOps: Azure AD Security Deep Dive安全研究https://posts.specterops.io/azure-ad-identity-attack-and-defense-handbook-9f281e1c704e
11CyberArk: Privileged Access Security Best Practices安全指南https://www.cyberark.com/resources/blog
12HashiCorp Vault: Identity Security官方文档https://developer.hashicorp.com/vault/docs

总结

数字身份与访问管理安全取证是现代应急响应中最复杂的领域之一。本文从云 IAM 配置滥用、OAuth/OIDC 协议攻击、MFA 绕过、PAM 攻防、身份联邦攻击五个维度,系统性地构建了 DIAM 安全取证的完整方法论。

核心要点回顾:

维度关键检测手段证据类型取证优先级
云 IAMCloudTrail/Audit Log 策略变更监控一级证据🔴 立即
OAuth/OIDC授权码注入和令牌重放检测二级证据🔴 立即
MFA疲劳攻击和地理不可能检测一级证据🔴 立即
PAMJIT 提权和批量凭据访问监控一级证据🔴 立即
联邦信任SAML 签名验证和 Golden Ticket 检测一级证据🔴 立即

身份安全事件的取证分析需要跨域关联多个日志源,建立统一的时间线,并结合行为基线模型识别偏离正常模式的异常活动。通过本文提供的 Sigma 规则、Bash 审计脚本和 Python 分析工具,取证人员可以快速构建起身份安全事件的检测和分析能力。