“침입당했을 때 PowerShell이 악용되었다”는 보고가 이어진 결과, 사내에서 PowerShell의 사용을 전면 금지하려는 조직이 있습니다. 그러나 이는 실효성 면에서도 업무 영향 면에서도 수지가 맞지 않습니다. PowerShell은 Windows의 관리 기반 그 자체이며, 멈추면 운영 자동화가 멈춥니다. 반면 공격 측은 같은 것을 다른 수단으로 실행할 수 있습니다.
현실적인 방침은 금지가 아니라 가시화와 제한입니다. 다행히 PowerShell 5.0 이후에는 방어 측을 위한 기능이 갖추어져 있습니다. 난독화된 코드라도 전개 후의 형태로 기록하는 스크립트 블록 로그, 실행 전에 백신 제품으로 내용을 전달하는 AMSI(Antimalware Scan Interface, 스크립트 내용을 백신 제품에 검사시키는 Windows의 구조), 실행 가능한 구문 자체를 제한하는 언어 모드, 그리고 「필요한 작업만 위임하는」 JEA. 이들을 조합하면 업무를 멈추지 않고 감사 가능한 상태를 만들 수 있습니다.
이 글에서는 사내 Windows 환경에서 PowerShell을 안전하게 계속 사용하기 위해 설정해야 할 항목을, 효과가 큰 순서로 정리합니다. 실행 정책과 서명에 대해서는 「PowerShell의 실행 정책과 스크립트 서명」에서 다루므로, 본 글은 그 앞의 내용을 다룹니다.
대상 환경·전제 조건
| 항목 | 내용 |
|---|---|
| 대상 OS | Windows 10/11, Windows Server. AMSI는 Windows 10 이후가 전제입니다1 |
| 대상 버전 | Windows PowerShell 5.1과 PowerShell 7 양쪽을 대상으로 합니다. 정책 설정 키도 로그 기록 위치도 5.1과 7에서 다르므로, 양쪽이 설치된 단말에서는 양쪽 모두에 설정하십시오(3장·4장) |
| 필요한 권한 | 3장~4장의 로그 설정·PowerShell 2.0 무효화, 7장의 JEA 엔드포인트 등록은 모두 관리자 권한이 필요합니다. 실제 운영에서는 한 대씩 수동으로 넣지 않고 그룹 정책으로 배포합니다 |
| 로컬/원격 | 3장~6장은 대상 단말·서버 내에서 완결됩니다. 7장의 JEA만 PowerShell 원격 처리(WinRM)가 활성화되어 있는 것이 전제입니다(7장 서두 참조) |
| 다루지 않는 내용 | 실행 정책과 서명의 자세한 내용은 별도 글(PowerShell의 실행 정책과 스크립트 서명)에서 다룹니다. 본 글에서는 위치 정리만 합니다(5장) |
1. 먼저 결론
- 최우선은 스크립트 블록 로그의 활성화입니다. 실행된 코드가 이벤트 로그에 기록되며, 난독화된 코드도 전개 후의 형태로 남습니다(이벤트 ID 4104).2
- 트랜스크립션도 함께 활성화합니다. 입출력을 포함한 세션 기록을 쓰기 전용 공유에 집계할 수 있습니다.2
- 로그를 활성화했다면 로그 크기 설정도 재검토합니다. 기본값 그대로면 단기간에 덮어써집니다.2
- AMSI에 의해 실행 전 스크립트가 백신 제품으로 전달됩니다. PowerShell 5.0 이후, Windows 10 이후에서 유효합니다.1
- 구버전 PowerShell 2.0 엔진은 무효화합니다. 남아 있으면 로그도 AMSI도 통하지 않는 구버전 엔진으로 전환될 여지가 남습니다.3
- 실행 정책은 보안 경계가 아닙니다. 공식이 명시하고 있습니다. 방어의 축으로 삼지 마십시오.4
- 언어 모드는 WDAC/AppLocker의 결과로 사용합니다. 수동 설정은 우회 가능하며 보안 기능으로 작동하지 않습니다.5
- 권한 위임에는 JEA. 「이 명령의, 이 매개변수만」을 허용하고, 관리자 권한을 나눠주지 않고 운영을 돌릴 수 있습니다.6
2. 무엇을 지키는가 ── 가시화가 먼저, 제한이 나중
이 글에서 다루는 4가지 대책은 각각 다른 계층을 담당합니다. 먼저 전체 그림입니다.
flowchart TB
U["관리자·헬프데스크·자동화 스크립트<br/>、그리고 침입한 공격자"]
DEL["【위임】JEA、7장<br/>관리자 권한을 나눠주지 않고 허용된 작업만 전달"]
LIM["【제한】언어 모드 + WDAC/AppLocker、6장<br/>실행 가능한 코드와 사용 가능한 구문을 좁힘"]
SCAN["【검사】AMSI、4장<br/>실행 직전의 내용을 백신 제품으로 전달<br/>PowerShell 2.0 무효화로 우회 경로를 막음"]
RUN["PowerShell에서의 조작"]
REC["【기록】스크립트 블록 로그 4104·트랜스크립션、3장<br/>난독화를 해제한 형태로 남김"]
U --> DEL
DEL --> LIM
LIM --> SCAN
SCAN --> RUN
RUN --> REC
기록은 「나중에 추적 가능한 상태」를 만들고, 위임은 권한 자체를 작게 만들며, 검사와 제한은 실행 직전에 막습니다. 4가지는 서로의 대체재가 아니라 담당하는 계층이 다릅니다. 그 위에서 도입 순서에는 우선순위가 있습니다.
| 단계 | 할 일 | 효과 |
|---|---|---|
| 1. 가시화 | 스크립트 블록 로그, 트랜스크립션 | 무슨 일이 일어났는지 알 수 있음. 사후 조사가 가능해짐 |
| 2. 기초적인 무력화 | PowerShell 2.0 무효화, 최신판 유지, AMSI 확인 | 검사를 우회하는 경로를 막음 |
| 3. 권한의 제한 | JEA에 의한 위임, 관리자 권한 축소 | 피해 범위를 한정 |
| 4. 실행의 제한 | WDAC/AppLocker + 제약된 언어 모드 | 미승인 코드의 실행 자체를 막음 |
많은 현장에서는 1과 3만 해도 상황이 크게 개선됩니다. 4는 도입 비용이 높기 때문에 업무 영향을 검증하면서 진행할 영역입니다.
3. 로그를 활성화한다 ── 4104가 가장 중요
PowerShell의 로그에는 3가지 계층이 있습니다.2
| 종류 | 기록 내용 | 이벤트 ID |
|---|---|---|
| 모듈 로그 | 지정한 모듈의 파이프라인 실행 상세 | 4103 |
| 스크립트 블록 로그 | 실행된 코드의 텍스트(난독화 해제 후) | 4104 |
| 기록 대상 로그 | 5.1은 Microsoft-Windows-PowerShell/Operational, 7은 PowerShellCore/Operational |
─ |
| 트랜스크립션 | 세션의 입출력을 텍스트 파일로 기록 | ─(파일 출력) |
모두 그룹 정책의 「컴퓨터 구성 > 관리 템플릿 > Windows 구성 요소 > Windows PowerShell」에서 설정할 수 있습니다. 레지스트리로 직접 설정하는 것도 가능합니다.2
# 스크립트 블록 로그를 활성화한다(관리자 권한 필요. 보통은 GPO로 배포)
$key = 'HKLM:\SOFTWARE\Policies\Microsoft\Windows\PowerShell\ScriptBlockLogging'
New-Item -Path $key -Force | Out-Null
# -Type은 레지스트리 공급자가 추가하는 동적 매개변수. 값의 형(DWord)을 명시할 수 있다
Set-ItemProperty -Path $key -Name 'EnableScriptBlockLogging' -Value 1 -Type DWord
# PowerShell 7(pwsh)은 별도의 정책 키를 본다. 양쪽에 설정한다
$key = 'HKLM:\SOFTWARE\Policies\Microsoft\PowerShellCore\ScriptBlockLogging'
New-Item -Path $key -Force | Out-Null
Set-ItemProperty -Path $key -Name 'EnableScriptBlockLogging' -Value 1 -Type DWord
# 트랜스크립션을 활성화하고, 쓰기 전용 공유로 집계한다
$key = 'HKLM:\SOFTWARE\Policies\Microsoft\Windows\PowerShell\Transcription'
New-Item -Path $key -Force | Out-Null
Set-ItemProperty -Path $key -Name 'EnableTranscripting' -Value 1 -Type DWord
Set-ItemProperty -Path $key -Name 'OutputDirectory' -Value '\\logsrv\pstranscripts$'
Set-ItemProperty -Path $key -Name 'EnableInvocationHeader' -Value 1 -Type DWord
# 트랜스크립션도 PowerShell 7 쪽은 별도 키. 같은 값을 쓴다
$key = 'HKLM:\SOFTWARE\Policies\Microsoft\PowerShellCore\Transcription'
New-Item -Path $key -Force | Out-Null
Set-ItemProperty -Path $key -Name 'EnableTranscripting' -Value 1 -Type DWord
Set-ItemProperty -Path $key -Name 'OutputDirectory' -Value '\\logsrv\pstranscripts$'
Set-ItemProperty -Path $key -Name 'EnableInvocationHeader' -Value 1 -Type DWord
PowerShell 7은 Windows\PowerShell 하위가 아니라 PowerShellCore 하위의 정책을 봅니다. 5.1 쪽만 설정하고 안심하면, pwsh에서 실행된 스크립트의 기록이 통째로 빠집니다. 스크립트 블록 로그·트랜스크립션 모두 양쪽 키에 같은 값을 써 넣으십시오(GPO로 배포하는 경우도 각각의 템플릿에서 설정합니다).
스크립트 블록 로그의 가치는 난독화에 대한 내성에 있습니다. Base64로 인코딩된 명령이나 문자열 연결로 조립된 코드라도, 실행 시점에 전개된 내용이 기록됩니다.2 공격 조사에서 「무엇이 실행되었는지」를 재현할 수 있는지는 이 설정 유무로 결정됩니다.
기록 확인은 Get-WinEvent입니다(「Get-WinEvent로 이벤트 로그를 실무적으로 조사하기」).
# 최근 1일의 스크립트 블록 로그를 확인한다.
# Windows PowerShell(5.1)과 PowerShell 7은 기록되는 로그가 다르므로
# 양쪽을 대상으로 한다
Get-WinEvent -FilterHashtable @{
LogName = 'Microsoft-Windows-PowerShell/Operational', 'PowerShellCore/Operational'
ID = 4104
StartTime = (Get-Date).AddDays(-1)
} -ErrorAction SilentlyContinue |
Select-Object TimeCreated, LogName, @{ n='Script'; e={ $_.Message } } -First 20
활성화되었는지의 판단은 다음 3가지로 합니다.
- 설정을 넣은 후, 새 PowerShell 세션을 연다. 스크립트 블록 로그는 활성화 이후에 시작된 세션부터 기록됩니다.2 이미 열려 있는 창에서 시도해 보고 「나오지 않는다」고 판단하지 마십시오.
- 표식이 될 명령을 새 세션에서 실행하고, 그것이 4104로 반환되는지 본다. 위 쿼리에서 방금 실행한 코드 조각을 포함한 이벤트가 반환되면 유효합니다. 하나도 반환되지 않으면
Get-ItemProperty로 정책 키의 값(EnableScriptBlockLogging이1)과, 5.1 쪽/7 쪽 어느 키에 썼는지를 확인합니다. - 로그 기록 대상을 잘못 보고 있지 않은지.
pwsh로 실행했다면 기록 대상은PowerShellCore/Operational입니다.Microsoft-Windows-PowerShell/Operational만 보고 「기록되지 않는다」고 오판하는 것이, 이 설정에서 가장 흔한 착각입니다.
활성화했다면 반드시 로그 크기를 재검토하십시오. 기본 크기 그대로면 몇 시간~며칠 만에 한 바퀴 돌아, 정작 필요할 때 남아 있지 않습니다.
Get-WinEvent -ListLog 'Microsoft-Windows-PowerShell/Operational', 'PowerShellCore/Operational' |
Select-Object LogName, MaximumSizeInBytes, RecordCount
wevtutil sl Microsoft-Windows-PowerShell/Operational /ms:1073741824 # 1GB로 확장하는 예
wevtutil sl PowerShellCore/Operational /ms:1073741824 # PowerShell 7 쪽도 잊지 말 것
# 반영되었는지 확인한다. MaximumSizeInBytes가 1073741824가 되어 있으면 성공
Get-WinEvent -ListLog 'Microsoft-Windows-PowerShell/Operational', 'PowerShellCore/Operational' |
Select-Object LogName, MaximumSizeInBytes
wevtutil sl은 성공해도 아무것도 표시하지 않으므로, 설정 전후로 MaximumSizeInBytes를 비교하는 것이 유일한 확인 수단입니다. 관리자 권한으로 실행하지 않으면 변경이 거부되므로, 값이 바뀌지 않을 때는 먼저 그것을 의심하십시오.
트랜스크립트의 집계 위치를 「쓸 수는 있지만 지울 수는 없는」 공유로 만든다
트랜스크립트의 출력 위치는 사용자가 쓰기는 할 수 있지만 삭제는 할 수 없는 공유로 만드는 것이 핵심입니다. 로컬에 두면 침해된 단말에서는 지워집니다.
권한은 공유 액세스 권한과 NTFS 액세스 권한 양쪽을 설정합니다(실효 권한은 둘 중 더 엄격한 쪽이 됩니다). 요점은 사용자에게 「폴더에 대한 파일 생성」과 「자신이 만든 파일의 읽기·쓰기」만 부여하고, 삭제(DE)와 하위 폴더·파일 삭제(DC)는 부여하지 않는 것입니다.
먼저 이 구성으로 무엇을 지킬 수 있고 무엇을 지킬 수 없는지를 명확히 해 둡니다. 지킬 수 있는 것은 「타인의 기록에는 일절 손댈 수 없음」과 「자신의 기록도 지울 수 없음」의 2가지입니다. 지킬 수 없는 것은 「자신의 기록 덮어쓰기」로, 이는 ACL만으로는 막을 수 없습니다(이유는 뒤에서 설명합니다). 그래도 탈취된 계정으로부터 타인의 증거를 지울 수 없게 되는 것만으로도 피해의 구분은 크게 달라집니다. 단말 한 대가 뚫려도 다른 단말의 기록은 남습니다.
# 로그 집계 서버 측에서 실행한다(관리자 권한 필요)
$path = 'D:\PSTranscripts'
$null = New-Item -Path $path -ItemType Directory -Force
# 공유를 만든다. 사용자 측은 변경(쓰기)까지, 관리는 로그 담당자만
New-SmbShare -Name 'pstranscripts$' -Path $path `
-ChangeAccess 'EXAMPLE\Domain Users' -FullAccess 'EXAMPLE\로그관리자'
# 상위 폴더로부터의 상속을 끊는다(두 번째 인수 $false = 상속했던 ACE를 물려받지 않음).
# 상속으로 내려오는 CREATOR OWNER의 완전 제어가 남아 있으면
# 「자신이 만든 파일은 자신이 지울 수 있다」 상태가 되어 집계의 의미가 없어진다.
#
# 다만 SetAccessRuleProtection이 제거하는 것은 「상속해 온 ACE」뿐이며,
# 그 폴더에 직접 부여된 ACE는 남는다. 위의 New-Item -Force는
# 기존 폴더에서도 성공하므로, 이전에 Domain Users나 Everyone에게
# 변경 권한을 직접 부여해 둔 적이 있다면, 그것은 이후 행을 모두 실행해도
# 그대로 살아남는다. 사용자는 타인의 트랜스크립트를 덮어쓰거나 삭제할 수 있는 채로 남는다.
# 그래서 직접 부여를 일단 모두 걷어낸 뒤, 필요한 것만 다시 넣는다
$acl = Get-Acl -Path $path
$acl.SetAccessRuleProtection($true, $false)
foreach ($ace in @($acl.Access)) { [void]$acl.RemoveAccessRuleSpecific($ace) }
# 완전히 걷어낸 상태에서, 관리 측과 SYSTEM을 같은 $acl에 넣은 뒤 한 번에 적용한다.
# 「비우기」와 「다시 넣기」를 따로 Set-Acl 하면, 아무도 손댈 수 없는 순간이 생긴다
foreach ($id in @('EXAMPLE\로그관리자', 'SYSTEM')) {
$acl.AddAccessRule([System.Security.AccessControl.FileSystemAccessRule]::new(
$id, 'FullControl', 'ContainerInherit, ObjectInherit', 'None', 'Allow'))
}
Set-Acl -Path $path -AclObject $acl
# 사용자에게는 폴더에 대한 "생성"만 허용한다. (OI)를 붙이지 않는 것이 핵심으로,
# 이 허용은 하위 파일에는 상속되지 않는다
# WD=파일 생성 AD=폴더 생성 X=폴더 탐색 RA=속성 읽기
# (OI)를 붙이면 「기존 파일에 대한 데이터 쓰기」까지 부여해 버린다
icacls $path /grant 'EXAMPLE\Domain Users:(CI)(WD,AD,X,RA)'
# 작성자가 「자신이 만든 파일만」 읽고 쓸 수 있게 한다.
# CREATOR OWNER는 파일 생성 시 「그 작성자」로 치환되므로
# 타인의 파일에는 적용되지 않는다. DE(삭제)는 부여하지 않는다
# RD,WD,AD = 데이터 읽기·쓰기·추가 RA,WA = 속성 읽기·쓰기
# REA,WEA = 확장 속성 읽기·쓰기 RC = 액세스 권한 읽기
# S = 동기화. GENERIC_READ/GENERIC_WRITE에 포함되므로 뺄 수 없다
# AD(추가)만으로 좁히고 싶어지지만, 그러면 트랜스크립트가 한 줄도 써지지 않는다(뒤에서 설명)
icacls $path /grant 'CREATOR OWNER:(OI)(IO)(RD,WD,AD,REA,WEA,RA,WA,RC,S)'
# 여기가 핵심. 위의 행만으로는 부족합니다.
# 파일을 만든 사용자는 그 파일의 "소유자"가 됩니다.
# Windows는 소유자에게 READ_CONTROL과 WRITE_DAC을 암묵적으로 부여하므로,
# WD도 DE도 부여하지 않았는데도 소유자는 스스로 DACL을 다시 써서
# 자신에게 덮어쓰기·삭제 권한을 재부여할 수 있습니다. 즉 탈취된 계정은
# 자신의 증거를 지울 수 있습니다. OWNER RIGHTS의 ACE가 있을 때 시스템은
# 소유자에 대한 암묵적 READ_CONTROL/WRITE_DAC을 무시합니다
icacls $path /grant 'OWNER RIGHTS:(OI)(IO)(RA,REA)'
icacls $path # 설정 결과를 확인한다
기존 폴더를 재사용할 때는 직접 부여된 ACE를 반드시 씻어내십시오. SetAccessRuleProtection($true, $false)가 제거하는 것은 상속해 온 ACE뿐이며, 그 폴더에 직접 붙어 있는 ACE는 그대로 남습니다.7 New-Item -Force는 기존 폴더에서도 성공하므로, “예전에 일시적으로 Domain Users에게 변경 권한을 부여한 적이 있다”는 경위가 있는 폴더를 재사용하면, 이후 CREATOR OWNER를 좁혀도 그 직접 부여만 살아남아 사용자는 타인의 트랜스크립트를 덮어쓰거나 삭제할 수 있습니다. 위 코드에서 일단 전부 걷어낸 뒤 다시 넣는 것은 이 때문입니다. 새 폴더를 만든다면 불필요하지만, 넣어 둬서 손해 볼 것은 없습니다. 적용 후 icacls $path의 출력을 눈으로 확인하고, 의도하지 않은 주체가 나열되어 있지 않은지 살펴보십시오.
여기서 (OI)를 붙이는 방식이 대책의 성패를 가릅니다. 사용자에 대한 허용에 (OI)를 붙여 WD를 부여하면 그 ACE가 하위의 모든 파일에 상속되어, 이름만 알면 타인의 트랜스크립트를 덮어쓰거나 잘라낼 수 있게 됩니다.7 그래서는 「단말이나 계정이 탈취된 후에도 기록이 남는다」는 목적을 달성할 수 없습니다. 위와 같이 폴더에 대한 생성 권한(상속시키지 않음)과, CREATOR OWNER를 통한 자신이 만든 파일만에 대한 읽기·쓰기로 나눕니다.
그리고 OWNER RIGHTS 행을 빠뜨리지 마십시오. 파일을 만든 사용자는 그 파일의 소유자가 됩니다. Windows는 소유자에게 READ_CONTROL과 WRITE_DAC을 암묵적으로 부여하므로,8 ACE로 DE를 부여하지 않았더라도 소유자는 스스로 DACL을 다시 써서 자신에게 삭제 권한을 재부여할 수 있습니다. 「자신의 기록은 자신이 지울 수 있는」 상태로는 CREATOR OWNER를 좁힌 의미가 없습니다. OWNER RIGHTS(S-1-3-4)의 ACE를 두면 시스템은 소유자에 대한 암묵적 READ_CONTROL/WRITE_DAC을 무시하므로, 여기서 비로소 「쓸 수는 있지만 지울 수는 없는」 상태가 성립합니다.8
왜 AD(추가)만으로 좁히지 않는가
「덮어쓰기를 원치 않으니 CREATOR OWNER는 AD만 있으면 된다」고 생각하고 싶어집니다. 그렇게 하면 트랜스크립트가 한 줄도 남지 않습니다.
Windows에서 「추가만 가능」이 성립하는 것은 작성자가 FILE_APPEND_DATA를 단독으로 지정해 파일을 열었을 때뿐입니다. Microsoft의 레퍼런스는 FILE_APPEND_DATA를 「(로컬 파일의 경우, FILE_WRITE_DATA를 동반하지 않고 이 플래그를 지정하면 쓰기 작업은 기존 데이터를 덮어쓰지 않는다)」로 정의하고 있습니다.9 즉 추가 전용은 여는 방식의 성질이지, ACE에 AD를 두는 것만으로 실현되는 것이 아닙니다.
그리고 Start-Transcript의 쓰기 입구는 추가 전용으로 열리지 않습니다. PowerShell의 구현은 먼저 FileMode.OpenOrCreate + FileAccess.ReadWrite로 열고, 실패했을 때만 FileMode.Append + FileAccess.Write로 다시 엽니다.10 .NET 쪽에서는 FileAccess.Read/Write가 각각 GENERIC_READ/GENERIC_WRITE로 사상되며, FileMode.Append도 내부적으로 FileMode.OpenOrCreate로 치환한 뒤 끝으로 시크할 뿐입니다.11 FILE_APPEND_DATA를 단독으로 요청하는 경로는 없습니다.
FILE_GENERIC_WRITE에는 FILE_WRITE_DATA·FILE_WRITE_ATTRIBUTES·FILE_WRITE_EA·READ_CONTROL·SYNCHRONIZE가, FILE_GENERIC_READ에는 FILE_READ_DATA·FILE_READ_ATTRIBUTES·FILE_READ_EA·READ_CONTROL·SYNCHRONIZE가 포함됩니다.9 액세스 검사는 요청한 모든 권리를 확인하므로, AD,RA,REA만 가진 사용자의 이 CreateFile은 거부됩니다. 공유 측에서 변경(Change)을 허용하고 있어도 NTFS 측에서 걸립니다. 기록을 지키려는 의도로 기록 자체를 멈추게 되는 것입니다.
그래서 이 절의 ACL은 AD 단독이 아니라 「읽기·쓰기는 허용하지만 DE(삭제)·WDAC(권한 변경)·WO(소유자 변경)는 부여하지 않는」 형태로 되어 있습니다. 소유자는 자신의 트랜스크립트를 덮어쓰거나 잘라낼 수 있습니다. 이 부분은 ACL로는 막을 수 없다고 받아들이십시오.
덮어쓰기까지 막고 싶다면, 작성자가 손댈 수 없는 위치로 내보내는 수밖에 없습니다. 선택지는 3가지입니다.
- JEA라면, 세션 구성 파일의
TranscriptDirectory를 사용한다(7장). 이 출력을 쓰는 것은 접속한 사용자가 아니라 Local System이며, Microsoft 문서도 「일반 사용자는 이 폴더에 대한 액세스 권한을 갖지 말 것」, 「감사하는 보안 관리자만으로 좁힐 것」을 요구합니다.12 사용자에게 애초에 권한이 필요 없으므로, 이 절의 고민 자체가 사라집니다 - Windows 이벤트 전달이나 SIEM으로 수집한다. 스크립트 블록 로그(4104)는 이벤트 로그로 나오므로 전달 대상으로 삼을 수 있습니다
- 추가 전용으로 여는 독자적인 수집 프로세스를 세운다.
FILE_APPEND_DATA를 단독으로 요청해 여는 부분까지 직접 작성하면, ACE 쪽의AD가 비로소 의미를 가집니다
또한 ACL로 지킬 수 있는 것은 어디까지나 「그 공유에 쓰는 사용자」로부터입니다. 파일 서버의 관리자는 소유권을 취득할 수 있고, 단말 측이 완전히 장악되어 있다면 애초에 공유에 써 내보내기 전에 손을 댈 수 있습니다. 이 절의 권한 설계는 「같은 공유를 사용하는 사용자끼리 서로 지울 수 없게 한다」를 담보하는 것이라고 이해하십시오.
icacls의 권한 기호(WD AD DE DC 등)와 상속 지정((OI) (CI) (IO))의 의미는 Windows 명령어 레퍼런스에 목록이 있습니다.7 참고로 이 구성은 반드시 검증기에서 한 번 시험해 본 뒤 배포하십시오. 트랜스크립트는 실행 중인 세션이 같은 파일에 계속 추가해 나가므로, 권한을 지나치게 좁히면 기록 자체가 떨어져 나갑니다. 「사용자 단말에서 쓸 수 있는지」, 「사용자가 타인의 기존 파일을 읽지 못하고 고쳐 쓰지 못하는지」, 「사용자가 자신의 파일을 삭제할 수 없는지」 3가지를 실제로 확인하는 것이 확실합니다.
4. 우회 경로를 막는다 ── PowerShell 2.0과 AMSI
AMSI(Antimalware Scan Interface)에 의해 PowerShell 5.0 이후에서는 스크립트 내용이 실행 직전에 백신 제품으로 전달되어, 난독화를 해제한 상태로 검사됩니다.1 Microsoft Defender를 포함한 대응 제품을 사용하고 있다면 추가 설정 없이 작동합니다.
문제는 구버전 엔진에는 이 구조가 없다는 것입니다. Windows PowerShell 2.0 엔진이 활성 상태로 남아 있으면, powershell.exe -Version 2에 의해 로그도 AMSI도 통하지 않는 환경으로 전환될 여지가 남습니다. 이 기능은 지원 종료(비권장) 상태이며 무효화가 권장됩니다.3
# PowerShell 2.0 엔진의 상태를 확인하고 무효화한다(관리자 권한 필요. 재부팅이 필요할 수 있음)
Get-WindowsOptionalFeature -Online -FeatureName MicrosoftWindowsPowerShellV2* |
Select-Object FeatureName, State
Disable-WindowsOptionalFeature -Online -FeatureName MicrosoftWindowsPowerShellV2Root -NoRestart
참고로 PowerShell 7(pwsh)을 도입한 경우 설정 키도 로그 기록 위치도 5.1과는 다릅니다(정책은 PowerShellCore 하위, 로그는 PowerShellCore/Operational). 양쪽이 사용되는 환경에서는 설정·로그 크기·조사 쿼리 모두를 양쪽에 대해 수행하십시오. 버전 공존에 대해서는 「Windows PowerShell 5.1과 PowerShell 7의 차이」를 참조하십시오.
5. 실행 정책의 위치를 착각하지 않는다
다시 한번 명확히 해 둡니다. 실행 정책은 보안 경계가 아닙니다. 공식 문서는 사용자가 의도치 않게 스크립트를 실행해 버리는 것을 막기 위한 안전 기능이며, 악의적인 조작을 막는 것이 아니라고 명시하고 있습니다.4
그렇다고는 해도 무가치한 것은 아닙니다. 서명 운영에는 「배포한 모듈이 변조되지 않았음을 확인할 수 있다」는 별도의 가치가 있습니다(「PowerShell 모듈의 사내 배포와 갱신」). 역할을 올바르게 이해하고 사용하는 것이 중요하며, 「AllSigned로 했으니 안전하다」는 이해가 가장 위험합니다.
6. 언어 모드 ── 애플리케이션 제어와 조합해서 사용한다
PowerShell 세션에는 언어 모드가 있어 사용 가능한 언어 요소가 제한됩니다.5
| 모드 | 사용 가능한 것 | 실무에서의 위치 |
|---|---|---|
| FullLanguage | 모든 언어 요소(기본값) | 일반 세션 |
| ConstrainedLanguage | 코드릿은 모두 동작하고 반복문·조건 분기·문자열 전개·속성 참조도 사용 가능. 다만 사용 가능한 .NET 형식이 허용 목록으로 한정되고, Add-Type은 서명된 어셈블리만 로드 가능 |
WDAC/AppLocker 하에서 자동으로 전환되는 모드. 대화형 조작과 일반적인 관리 작업은 대체로 그대로 수행 가능 |
| RestrictedLanguage | 명령은 실행 가능하지만 스크립트 블록을 사용할 수 없음. 변수는 $PSCulture $PSUICulture $true $false $null만, 비교 연산자는 -eq -gt -lt만 가능하며 대입·속성 참조·메서드 호출은 불가 |
모듈 매니페스트(.psd1) 로드에 사용되는 모드. 사람이 대화형으로 사용할 것을 전제하지 않음 |
| NoLanguage | 스크립트 언어 자체가 무효. 스크립트도 변수도 사용할 수 없으며 코드릿과 네이티브 명령 호출만 가능 | JEA 세션 구성(RestrictedRemoteServer)의 기본값. 「정해진 명령만 두드리는」 창구를 만들기 위한 모드 |
3가지 제한 모드는 단계가 아니라 용도가 다릅니다. ConstrainedLanguage는 「사람이 스크립트를 작성해 사용하는 환경을, 위험한 형식 사용만 막는」 것이고, NoLanguage는 「애초에 스크립트를 작성하지 못하게 하는」 것으로, 후자는 JEA와 같은 한정된 창구에서만 성립합니다.5
현재 모드는 다음으로 확인할 수 있습니다.
$ExecutionContext.SessionState.LanguageMode
중요한 것은 설정 방법입니다. 제약된 언어 모드는 WDAC(Windows Defender Application Control)나 AppLocker로 허용 목록 방식의 애플리케이션 제어를 구성했을 때 PowerShell이 자동으로 전환되는 형태로 기능합니다.5 환경 변수 등으로 수동 설정하는 방법은 우회가 쉬우며 보안 기능으로는 작동하지 않습니다. 애플리케이션 제어를 도입하지 않고 언어 모드만 좁히는 것은 노력에 비해 효과가 없다고 이해하십시오.
7. JEA ── 「필요한 작업만」을 위임한다
실제 피해를 좌우하는 것은 많은 경우 권한의 넓이입니다. 「헬프데스크에 관리자 권한을 넘기고 있다」, 「운영 담당자가 전원 Domain Admin에 속해 있다」와 같은 상태에서는 한 대의 침해가 전사의 침해가 됩니다.
JEA(Just Enough Administration)는 관리자 권한을 넘기지 않고 특정 작업만 위임하는 장치입니다.6 「애플리케이션 서비스의 재시작만 헬프데스크에 맡긴다」와 같은 요구에 딱 맞습니다.
이 장만 전제가 다르다 ── JEA는 원격 처리 위에 성립한다
3장~6장이 로컬 설정으로 완결되는 것과 달리, JEA는 PowerShell 원격 처리(WinRM) 구조 그 자체 위에 만들어져 있습니다.13 「JEA로 위임한다」는 것은 대상 서버에 전용 접속처(세션 구성)를 등록하고, 그곳으로 접속시키는 것입니다. 자사에서 재현 가능한지 여부는 우선 여기서 결정됩니다.
필요한 것은 다음 3가지입니다.
| 전제 | 내용 |
|---|---|
| PowerShell 버전 | JEA는 PowerShell 5.0 이후에서 사용할 수 있습니다13 |
| 원격 처리 활성화 | 대상 서버에서 PowerShell 원격 처리가 활성화되어 있어야 합니다. Windows Server 2012 이후는 기본적으로 활성화되어 있으며, 비활성 상태라면 관리자 권한의 PowerShell에서 Enable-PSRemoting을 실행합니다13 |
| 배치 위치 | 역할 기능 파일(.psrc)은 대상 서버 상의 모듈 RoleCapabilities 폴더에, 세션 구성(.pssc)은 대상 서버 상에서 등록합니다. 사용자 단말에 두는 것이 아닙니다6 |
즉 아래의 순서 1~3은 위임받는 쪽 서버에서, 관리자 권한으로 수행하는 작업입니다. 사용자 단말에서 필요한 것은 그 서버에 접속할 수 있다는 것뿐입니다. 원격 처리 자체의 구성과 안전한 설정은 「PowerShell Remoting(WinRM) 입문」에 정리되어 있습니다.
순서 1: 역할 기능 파일(.psrc)로 허용할 작업을 정의한다
역할 기능 파일은 PowerShell 모듈의 RoleCapabilities 폴더에 있어야 합니다. 폴더만 만들어서는 모듈로 인식되지 않고, 뒤에서 나오는 RoleDefinitions에서 이름으로 해석할 수 없습니다. 그래서 먼저 그릇이 되는 모듈(매니페스트를 가진 폴더)을 만듭니다.6
# 역할 기능을 담을 모듈을 만든다(매니페스트가 필요)
$moduleRoot = 'C:\Program Files\WindowsPowerShell\Modules\KsJea'
$null = New-Item -Path "$moduleRoot\RoleCapabilities" -ItemType Directory -Force
# 모듈 폴더에는 폴더와 같은 이름의 파일이 1개 이상 필요
$null = New-Item -Path "$moduleRoot\KsJea.psm1" -ItemType File -Force
New-ModuleManifest -Path "$moduleRoot\KsJea.psd1" -RootModule 'KsJea.psm1'
# 역할 기능 파일의 틀을 만든다(파일명이 역할명이 된다).
# 역할명은 PSModulePath 상의 모든 모듈에서 「이름만으로」 해석되므로,
# 'HelpDesk'와 같은 일반적인 이름은 다른 모듈의 .psrc와 충돌할 수 있다.
# 충돌하면 어느 쪽이 선택될지 보장이 없어, 의도하지 않은 권한이 부여된다.
# 조직 접두사를 붙인 고유한 이름으로 한다
New-PSRoleCapabilityFile -Path "$moduleRoot\RoleCapabilities\KsHelpDesk.psrc"
# 모듈로 보이는지 확인한다
Get-Module -Name KsJea -ListAvailable
# KsHelpDesk.psrc(발췌) ── 「무엇을, 어디까지」 허용할지를 선언한다
@{
GUID = '....'
# 읽기 전용 명령은 그대로 보여줘도 좋다
VisibleCmdlets = @(
'Get-Service',
'Get-EventLog'
)
# 상태를 바꾸는 Restart-Service는 일부러 VisibleCmdlets에 넣지 않는다(뒤에서 설명)
# VisibleFunctions는 「세션에 로드되어 있는 함수」를 추려낼 뿐,
# 함수를 정의하지는 않는다. 독자적인 함수는 FunctionDefinitions에서 내용을 작성하고,
# 그 위에서 VisibleFunctions에도 이름을 올린다(둘 다 필요)
VisibleFunctions = @('Get-KsAppStatus', 'Restart-KsAppService')
FunctionDefinitions = @(
@{
Name = 'Get-KsAppStatus'
ScriptBlock = {
# 함수 본체는 기본 언어 모드로 동작하므로 JEA의 제약을 받지 않는다.
# 사용자의 입력을 그대로 위험한 명령에 전달하지 말 것
Get-Service -Name 'KsAppService' |
Microsoft.PowerShell.Utility\Select-Object Name, Status, StartType
}
},
@{
# 재시작은 「인수로만 대상을 받는」 함수로 공개한다
Name = 'Restart-KsAppService'
ScriptBlock = {
param(
[Parameter(Mandatory)]
[ValidateSet('KsAppService', 'Spooler')]
[string] $Name
)
Microsoft.PowerShell.Management\Restart-Service -Name $Name
}
}
)
VisibleExternalCommands = @()
}
VisibleCmdlets의 ValidateSet만으로는 상태를 바꾸는 명령을 안전하게 좁힐 수 없습니다. Parameters와 ValidateSet에 의한 제한은 그 매개변수가 실제로 바인딩되었을 때만 평가됩니다. Restart-Service는 ServiceController를 파이프라인으로 받을 수 있으므로,
Get-Service WinRM | Restart-Service
라고 쓰면 -Name이 바인딩되지 않아 ValidateSet은 그대로 통과합니다. 「KsAppService와 Spooler만 재시작할 수 있다」는 의도의 엔드포인트에서 임의의 서비스를 재시작할 수 있게 되는 것입니다.
같은 일이 다른 매개변수 집합에서도 일어납니다. Get-WinEvent에 LogName의 ValidateSet을 붙여도,
Get-WinEvent -ProviderName Microsoft-Windows-Security-Auditing -MaxEvents 1
라고 쓰면 LogName이 바인딩되지 않아 제한이 작동하지 않습니다. JEA 세션은 가상 관리자로 동작하므로 이 경우 Security 로그까지 읽을 수 있게 됩니다.
그래서 위 예시에서는 Restart-Service를 VisibleCmdlets에 넣지 않고, 인수로만 대상을 받는 래퍼 함수(Restart-KsAppService)만 공개하고 있습니다. 입력 경로가 인수뿐이라면 우회할 여지가 없습니다.
Parameters와 ValidateSet에 의한 제한은 「그 매개변수가 바인딩되었을 때」만 작동합니다. 파이프라인 입력이나 다른 매개변수 집합으로 바인딩을 회피할 수 있는 명령에서는 제한 자체가 무효화됩니다. 인수를 좁히고 싶은 명령은 원칙적으로 래퍼 함수로 감싼다고 생각하십시오.
여기서 2가지, 걸려 넘어지기 쉬운 사양이 있습니다. 첫 번째는 VisibleFunctions가 함수를 만들지 않는다는 것입니다. 이름만 올린 함수는 세션에 존재하지 않으며 사용자에게는 「그런 명령은 없다」고 보입니다. 독자적인 함수는 FunctionDefinitions에서 정의한 뒤, VisibleFunctions에도 올리십시오.14 개수가 늘어난다면 스크립트 모듈로 분리해, 그 모듈의 함수를 VisibleFunctions로 공개하는 편이 관리하기 쉬워집니다.
두 번째는 함수 본체가 JEA의 제약을 받지 않는다는 것입니다.14 Select-Object 등 JEA가 바꿔치기하는 제약된 명령을 본래의 동작으로 사용하고 싶다면, 위와 같이 Microsoft.PowerShell.Utility\Select-Object와 같이 완전 정규화하여 호출합니다. 뒤집어 말하면 함수 안에서는 무엇이든 할 수 있다는 뜻이므로, 사용자의 입력을 Invoke-Expression에 넘기는 방식은 절대로 피하십시오.
순서 2: 세션 구성 파일(.pssc)로, 누구에게 어느 역할을 할당할지 정의한다
# SessionType: 제한된 원격 서버(기본 NoLanguage)
# RunAsVirtualAccount: 가상 관리자 계정으로 실행
# TranscriptDirectory: 실행 내용을 기록
$pssc = @{
Path = '.\KsHelpDesk.pssc'
SessionType = 'RestrictedRemoteServer'
RunAsVirtualAccount = $true
TranscriptDirectory = 'C:\JeaTranscripts'
RoleDefinitions = @{ 'EXAMPLE\HelpDesk' = @{ RoleCapabilities = 'KsHelpDesk' } }
}
New-PSSessionConfigurationFile @pssc
순서 3: 등록한다
Register-PSSessionConfiguration -Name 'KsHelpDesk' -Path .\KsHelpDesk.pssc -Force
사용자 측은 다음과 같이 접속합니다.
Enter-PSSession -ComputerName 'appsrv01' -ConfigurationName 'KsHelpDesk'
# 허용된 명령만 사용할 수 있다. Restart-Service도 지정된 서비스만
JEA의 요점은 3가지입니다.6
- 사용자는 관리자 권한을 갖지 않는다. 실행은 가상 계정 쪽에서 이루어진다
- 세션은 제한된 원격 서버로 구성된다. 기본적으로 언어 모드가 제한되어 임의 코드를 실행할 수 없다
- 트랜스크립트로 실행 내용이 기록된다. 누가 무엇을 했는지 감사할 수 있다
참고로 역할 기능 파일은 앞서 설명한 대로 모듈의 RoleCapabilities 폴더 아래에 있어야 하며, 그 모듈이 $env:PSModulePath에서 발견되는 것이 전제입니다. RoleDefinitions에서 지정한 역할명을 해석할 수 없을 때는 먼저 Get-Module -ListAvailable로 모듈이 보이는지 확인하십시오.6
등록한 엔드포인트는 Get-PSSessionConfiguration으로 목록을 볼 수 있습니다. 서두에 적었듯이 JEA는 원격 실행 구성이 전제이므로, 접속이 안 될 때는 JEA의 정의가 아니라 WinRM 쪽부터 나누어 확인하십시오(「PowerShell Remoting(WinRM) 입문」).
8. 실무 체크리스트
| 항목 | 우선도 | 상태 |
|---|---|---|
| 스크립트 블록 로그(4104)를 활성화했다 | 높음 | GPO로 전사 배포215 |
| PowerShell 관련 로그 크기를 확장했다 | 높음 | 기본값 그대로면 며칠 만에 사라짐 |
| 트랜스크립션을 활성화하고 쓰기 전용 공유에 집계했다 | 높음 | 단말 로컬에 두지 않음2 |
| PowerShell 2.0 엔진을 무효화했다 | 높음 | 로그·AMSI의 우회 경로3 |
| 백신 제품이 AMSI에 대응한다 | 높음 | 기본적으로 유효1 |
| 실행 정책을 보안 경계로 오해하지 않는다 | 중간 | 서명은 별도의 가치4 |
| 관리자 권한의 부여 범위를 점검했다 | 높음 | 피해 범위를 결정하는 것은 권한의 넓이 |
| 정형 작업을 JEA로 위임했다 | 중간 | 관리자 권한 축소로 직결6 |
| WDAC/AppLocker를 검토했다 | 중간 | 언어 모드 제한은 이것과 세트5 |
| PowerShell 7 쪽에도 로그 설정을 배포했다 | 중간 | 5.1과 7은 설정이 다름 |
9. 정리
- PowerShell 금지는 실효성이 낮고 업무를 멈춥니다. 방침은 「가시화와 제한」입니다.
- 최우선은 스크립트 블록 로그(4104)의 활성화. 난독화된 코드도 전개 후의 형태로 기록됩니다. 로그 크기 확장과 세트로 실시하십시오.
- 트랜스크립션은 사용자가 지울 수 없는 공유에 집계하는 것이 핵심입니다.
- PowerShell 2.0 엔진은 무효화합니다. 로그도 AMSI도 통하지 않는 경로를 남기지 않기 위해서입니다.
- 실행 정책은 보안 경계가 아닙니다. 서명은 변조 탐지로서 가치가 있지만, 방어의 축으로 삼지 마십시오.
- 권한의 넓이가 피해의 넓이를 결정합니다. JEA로 「필요한 작업만」을 위임하면 관리자 권한을 나눠주지 않고 운영을 돌릴 수 있습니다.
샘플 코드 다운로드
이 글에서 다룬 코드는 그대로 실행할 수 있는 형태로 묶어 배포하고 있습니다. 로그 설정 활성화와 JEA 엔드포인트 작성이 들어 있습니다.
이 글의 샘플은 Windows나 테넌트에 의존하기 때문에 실행 검증은 하지 않았습니다. 구문 분석과 PSScriptAnalyzer에 의한 정적 분석까지는 전체 파일에 대해 실시했지만, 동작은 반드시 직접 검증기에서 확인하십시오.
# 구문 분석 + 정적 분석(Windows 이외에서도 실행 가능)
./Invoke-SampleTests.ps1
설정값(경로, 서버명, 테넌트 ID 등)은 예시입니다. 그대로 운영 환경에서 실행하지 말고, 자사 환경에 맞게 바꿔서 사용하십시오.
관련 글
- PowerShell의 실행 정책과 스크립트 서명 ── ‘Bypass로 덮어버리는’ 운영에서 벗어나는 실무 가이드
- Get-WinEvent로 이벤트 로그를 실무적으로 조사하기 ── 필터링 속도가 조사 시간을 좌우한다
- PowerShell Remoting(WinRM) 입문 ── 여러 대의 Windows를 일괄 관리하기
- PowerShell에서 자격 증명을 안전하게 다루기 ── 평문 비밀번호를 스크립트에서 추방한다
- Windows 앱 개발에서 최저한의 보안을 지키기 위한 체크리스트
- 정보보안 10대 위협 2026 ── 랭킹을 보는 법과 중소기업이 정말 대비해야 할 것
관련 상담 영역
합동회사 코무라소프트에서는 Windows 운영 환경의 보안 설정 리뷰, 권한 위임(JEA)을 포함한 운영 설계, 감사 로그의 정비와 활용에 관한 상담을 다루고 있습니다.
참고 링크
-
Microsoft Learn, Antimalware Scan Interface (AMSI). AMSI가 애플리케이션과 서비스에서 임의의 백신 제품으로 콘텐츠를 전달해 검사시키는 구조라는 것, PowerShell을 포함한 Windows의 스크립트 엔진이 통합되어 있으며 난독화된 스크립트도 실행 시점의 내용으로 검사할 수 있다는 것에 대해. ↩ ↩2 ↩3 ↩4
-
Microsoft Learn, about_Logging_Windows. 모듈 로그·스크립트 블록 로그·트랜스크립션이라는 3종류의 로그 기능, 그룹 정책 및 레지스트리(HKLM\SOFTWARE\Policies\Microsoft\Windows\PowerShell 하위)에 의한 활성화, 스크립트 블록 로그가 이벤트 ID 4104로 난독화 해제 후의 코드를 기록하는 것, 모듈 로그가 이벤트 ID 4103으로 기록되는 것, 트랜스크립션의 OutputDirectory와 EnableInvocationHeader 설정에 대해. ↩ ↩2 ↩3 ↩4 ↩5 ↩6 ↩7 ↩8 ↩9
-
Microsoft Learn, Windows PowerShell 2.0의 지원 종료. Windows PowerShell 2.0 엔진이 지원 종료 상태이며 무효화가 권장되는 것, Windows의 옵션 기능으로 제공되어 활성/비활성을 전환할 수 있는 것에 대해. ↩ ↩2 ↩3
-
Microsoft Learn, about_Execution_Policies. 실행 정책이 보안 경계가 아니라 사용자가 의도치 않게 스크립트를 실행하는 것을 막기 위한 안전 기능이라는 것, 여러 우회 수단이 존재한다는 것에 대해. ↩ ↩2 ↩3
-
Microsoft Learn, about_Language_Modes. FullLanguage·ConstrainedLanguage·RestrictedLanguage·NoLanguage 각 모드에서 사용 가능한 언어 요소, $ExecutionContext.SessionState.LanguageMode에 의한 현재 모드 확인, WDAC나 AppLocker에 의한 애플리케이션 제어가 유효한 환경에서 PowerShell이 제약된 언어 모드로 동작하는 것, 수동 언어 모드 설정이 보안 기능으로 의도되지 않은 것에 대해. ↩ ↩2 ↩3 ↩4 ↩5
-
Microsoft Learn, Just Enough Administration (JEA) 개요. JEA가 관리자 권한을 부여하지 않고 특정 관리 작업만 위임하는 구조라는 것, 역할 기능 파일(.psrc)에 의한 VisibleCmdlets·매개변수와 ValidateSet에 의한 제한, 세션 구성 파일(.pssc)의 RestrictedRemoteServer·RunAsVirtualAccount·TranscriptDirectory·RoleDefinitions, Register-PSSessionConfiguration에 의한 등록, JEA 세션에서 언어 모드가 제한되는 것에 대해. 역할 기능 파일을 PowerShell 모듈의 RoleCapabilities 폴더에 배치해야 하는 것(모듈로 검출 가능한 상태로 해 둘 것, New-PSRoleCapabilityFile에 의한 작성)은 JEA Role Capabilities를 참조. ↩ ↩2 ↩3 ↩4 ↩5 ↩6 ↩7
-
Microsoft Learn, icacls. 파일과 폴더의 액세스 제어 목록을 표시·변경하는 명령이라는 것, /grant에 의한 권한 부여와 /remove:g에 의한 부여된 권한의 삭제, 권한 마스크에 지정할 수 있는 상세 권한 기호(DE=삭제, DC=하위 폴더와 파일 삭제, WD=데이터 쓰기/파일 생성, AD=데이터 추가/하위 폴더 생성, WA=속성 쓰기, WEA=확장 속성 쓰기, RA=속성 읽기, X=실행/탐색, F=완전 액세스 등), 상속 지정((OI)=객체 상속, (CI)=컨테이너 상속)에 대해. 공유 생성과 액세스 권한 지정은 New-SmbShare(-FullAccess / -ChangeAccess / -ReadAccess)를, 상속 무효화는 ObjectSecurity.SetAccessRuleProtection(첫 번째 인수로 상속을 보호하고, 두 번째 인수로 상속된 ACE를 물려받을지를 지정)을 참조. 이 메서드가 지정할 수 있는 것은 상속 ACE의 취급뿐이며, 그 객체에 직접 부여된 ACE는 대상이 아닙니다. 직접 부여를 제거하려면 ObjectSecurity.RemoveAccessRuleSpecific 등으로 개별적으로 제거합니다. ↩ ↩2 ↩3
-
Microsoft Learn, 특수 ID 그룹(Special Identity Groups).
OWNER RIGHTS(S-1-3-4)가 객체의 현재 소유자를 나타내는 그룹이며, 이 SID를 가진 ACE가 객체에 적용되어 있을 때 시스템은 소유자에 대한 암묵적READ_CONTROL과WRITE_DAC을 무시한다는 것에 대해. 뒤집어 말하면 이 ACE가 없으면 소유자는 암묵적으로 DACL을 다시 쓸 수 있습니다. ↩ ↩2 -
Microsoft Learn, File Access Rights Constants.
FILE_APPEND_DATA가 「For a file object, the right to append data to the file. (For local files, write operations will not overwrite existing data if this flag is specified without FILE_WRITE_DATA.)」로 정의되어 있으며, 추가 전용 쓰기는 「FILE_WRITE_DATA를 동반하지 않고FILE_APPEND_DATA를 지정해 열었을」 때 성립한다는 것,FILE_WRITE_DATA/FILE_ADD_FILE,FILE_DELETE_CHILD의 의미에 대해. 제네릭 액세스 권한 매핑(FILE_GENERIC_WRITE=FILE_APPEND_DATA+FILE_WRITE_ATTRIBUTES+FILE_WRITE_DATA+FILE_WRITE_EA+STANDARD_RIGHTS_WRITE+SYNCHRONIZE,FILE_GENERIC_READ=FILE_READ_ATTRIBUTES+FILE_READ_DATA+FILE_READ_EA+STANDARD_RIGHTS_READ+SYNCHRONIZE)는 File Security and Access Rights를 참조. ↩ ↩2 -
PowerShell,
TranscriptionOption.FlushContentToDisk(src/System.Management.Automation/engine/hostifaces/MshHostUserInterface.cs). 트랜스크립트의 쓰기 입구가new FileStream(this.Path, FileMode.OpenOrCreate, FileAccess.ReadWrite, FileShare.Read)로 열리고, 실패한 경우에만new FileStream(this.Path, FileMode.Append, FileAccess.Write, FileShare.Read)로 다시 열리는 것,FILE_APPEND_DATA를 단독으로 요청하는 경로가 없는 것에 대해. ↩ -
.NET,
SafeFileHandle.Open(src/libraries/System.Private.CoreLib/src/Microsoft/Win32/SafeHandles/SafeFileHandle.Windows.cs).FileAccess.Read/FileAccess.Write가 각각GENERIC_READ/GENERIC_WRITE로 사상되는 것,FileMode.Append가CreateFile을 호출하기 전에FileMode.OpenOrCreate로 치환될 뿐이며, 액세스 마스크에FILE_APPEND_DATA단독이 사용되는 일은 없는 것에 대해. ↩ -
Microsoft Learn, JEA 세션 구성. 세션 구성 파일의
TranscriptDirectory에 폴더를 지정하면 자동으로 트랜스크립트가 기록되는 것, 「Transcripts are written to the folder by the Local System account, which requires read and write access to the directory. Standard users should have no access to the folder. Limit the number of security administrators that have access to audit the transcripts.」(일반 사용자는 이 폴더에 대한 액세스 권한을 가져서는 안 되며, 감사하는 관리자만으로 좁힐 것)에 대해. ↩ -
Microsoft Learn, JEA의 전제 조건. JEA가 PowerShell 5.0 이후에서 사용 가능한 것, PowerShell 원격 처리가 JEA의 토대이며 JEA를 사용하기 전에 원격 처리가 활성화되고 적절히 보호되어 있어야 하는 것, Windows Server 2012 이후에서는 PowerShell 원격 처리가 기본적으로 활성화되어 있으며 활성화되어 있지 않은 경우 관리자 권한의 PowerShell 창에서 Enable-PSRemoting을 실행해 활성화할 수 있는 것에 대해. ↩ ↩2 ↩3
-
Microsoft Learn, JEA Role Capabilities. 독자적인 함수를 FunctionDefinitions에서 정의한 뒤 VisibleFunctions에도 이름을 올려야 하는 것(「Don’t forget to add the name of your custom functions to the VisibleFunctions field so they can be run by the JEA users.」), 함수의 본체(스크립트 블록)가 시스템 기본 언어 모드로 실행되어 JEA의 제약을 받지 않는 것, JEA가 바꿔치기하는 제약된 명령을 본래의 구현으로 사용하려면 완전 정규화된 이름(Microsoft.PowerShell.Utility\Select-Object)이 필요한 것, 함수가 많은 경우 스크립트 모듈로 분리해 VisibleFunctions로 공개하는 방법, 모듈 폴더에 폴더와 같은 이름의 파일이 필요한 것에 대해. ↩ ↩2
-
Microsoft Learn, Set-ItemProperty. 레지스트리 공급자 사용 시 -Type이 동적 매개변수로 추가되어 레지스트리 값의 데이터 형식(String / ExpandString / Binary / DWord / MultiString / QWord 등)을 지정할 수 있는 것에 대해. 값이 존재하지 않는 경우 생성되는 것을 포함. ↩
관련 기사
같은 태그를 공유하는 최신 기사입니다. 더 가까운 주제로 지식을 넓힐 수 있습니다.
Get-WinEvent로 이벤트 로그를 실무적으로 조사하기 ── 필터링 속도가 조사 시간을 좌우한다
Windows 이벤트 로그 조사를 PowerShell로 효율화하는 방법을 정리합니다. Where-Object로 좁히면 느린 이유, FilterHashtable과 XPath의 구분 사용, 재시작이나 로그온·앱 비정상 종료 조사 레시피, 여러 대에서...
Windows 인증서 저장소 실무 가이드 ── 사용자와 컴퓨터, 어느 쪽에 넣어야 하는가
클라이언트 인증서는 사용자와 컴퓨터 중 어느 저장소에 넣어야 할까. certmgr.msc와 certlm.msc의 차이, 비밀 키 권한 부여, PowerShell을 이용한 만료 점검까지, 인증서의 단골 사고를 체계적으로 없애는 실무 가이드입니다.
SMB 서명과 LDAP 채널 바인딩 ── NTLM 대책의 '나머지 절반'을 실무에서 마무리하기
NTLM을 중단하기까지의 기간 동안 릴레이 공격의 피해를 억제하는 방어책이 SMB 서명과 LDAP 서명·채널 바인딩입니다. OS별 기본값, 감사 이벤트를 읽는 법, 강제 적용으로 나아가는 절차, 업무 앱과 기기를 고치는 방법까지 실무 관점에서 정...
NTLM 폐지로 업무 앱이 멈추는가 ── 감사 로그를 얻는 방법과 의존을 없애는 순서
NTLM 폐지에 대비해 자사 Windows 환경과 업무 앱이 어디에서 NTLM에 의존하고 있는지 찾아내는 절차를 정리합니다. 감사 정책, NTLM/Operational 로그의 이벤트 8001~8004 추적, NTLM으로 떨어지는 전형적인 패턴과 ...
Write-Host를 그만두다 ── PowerShell의 출력 스트림과 로그 설계
PowerShell 6가지 출력 스트림의 구분 사용법, Write-Host가 안고 있는 문제와 올바른 쓰임새, 함수의 반환값이 오염되는 원인, -Verbose나 -InformationVariable을 통한 호출 측 제어, 구조화 로그를 남기는 방...
관련 토픽
이 기사와 가까운 토픽 페이지입니다. 기사를 출발점 삼아 관련 서비스와 다른 기사로 이어집니다.
Windows 기술 토픽
Windows 개발, 장애 조사, 기존 자산 활용에 관한 KomuraSoft LLC 기사를 모은 토픽 허브입니다.
이 주제와 연결되는 서비스
이 기사는 다음 서비스 페이지로 이어집니다. 가까운 입구부터 확인해 주세요.
Windows 앱 개발
상주 처리, 장비 연동, 운영 로그, 유지 보수 가능한 구조가 필요한 Windows 데스크톱 애플리케이션을 지원합니다.
자주 묻는 질문
이 기사 주제에 대해 상담 시 자주 나오는 질문을 모았습니다.
- 보안 대책으로 사내에서 PowerShell을 금지해야 할까요?
- 실효성이 낮고 부작용이 크기 때문에 권장하지 않습니다. PowerShell은 Windows의 관리 기반 그 자체이며, 실체는 .NET 위의 기능입니다. 실행 파일을 차단해도 같은 API를 다른 수단으로 호출하는 것이 가능하므로 공격자 입장에서는 장애가 되기 어려운 반면, 정규 관리 업무와 자동화는 확실히 멈춥니다. 현실적인 방침은 금지가 아니라 「가시화와 제한」입니다. 스크립트 블록 로그로 무엇이 실행되었는지 기록하고, 구버전을 무효화하며, 필요에 따라 언어 모드나 JEA로 실행 가능한 범위를 좁힙니다.
- 스크립트 블록 로그를 활성화하면 로그가 대량으로 나와서 곤란하지 않을까요?
- 실제로 늘어나므로 로그 크기 설정과 함께 활성화하십시오. Microsoft-Windows-PowerShell/Operational 로그의 최대 크기를 기본값 그대로 두면 단기간에 덮어써져 정작 필요할 때 남아 있지 않게 됩니다. 운영 상으로는 로그 크기를 충분히 확보하고, 필요하다면 SIEM이나 이벤트 전달로 집계합니다. 참고로 더 상세한 「호출 시작·종료」까지 기록하는 설정도 있지만, 이쪽은 출력량이 매우 많기 때문에 보통은 기본 스크립트 블록 로그만 활성화합니다.
- 실행 정책을 AllSigned로 하면 보안 대책이 될까요?
- 실행 정책은 보안 경계가 아닙니다. 공식 문서도 사용자가 의도치 않게 위험한 스크립트를 실행하는 것을 막기 위한 장치일 뿐, 악의적인 조작을 막는 것이 아니라고 명시하고 있습니다. 우회 방법이 여러 가지 존재하기 때문입니다. 서명 운영에는 배포물의 무결성을 확인할 수 있다는 별도의 가치가 있지만, 방어의 축은 로그에 의한 가시화, AMSI에 의한 검사, 언어 모드나 JEA에 의한 권한 제한에 두어야 합니다.
- 제약된 언어 모드(ConstrainedLanguage)는 수동으로 설정해도 되나요?
- 환경 변수 등으로 수동 설정하는 것은 권장되지 않습니다. 우회가 쉬워 보안 기능으로서 작동하지 않기 때문입니다. 제약된 언어 모드는 WDAC(Windows Defender Application Control)나 AppLocker로 애플리케이션 제어를 구성한 결과 PowerShell이 자동으로 전환되는 형태로 사용하는 것이 본래의 설계입니다. 애플리케이션 제어를 도입하지 않고 언어 모드만 좁혀도 실효성 있는 방어가 되지 않습니다.
- 헬프데스크 담당자에게 서버의 특정 서비스 재시작만 맡기고 싶습니다.
- JEA(Just Enough Administration)가 바로 그 용도를 위한 장치입니다. 역할 기능 파일에서 「이 명령의, 이 매개변수의, 이 값만」 허용한다고 정의하고 세션 구성으로 등록합니다. 사용자는 관리자 권한을 갖지 않은 채 가상 계정을 통해 허용된 작업만 실행할 수 있습니다. 세션은 제한된 원격 서버로 구성되며 기본적으로 언어 모드도 제한되므로 임의 코드가 실행될 여지가 없습니다. 실행 내용은 트랜스크립트로 기록할 수 있습니다.