A native Go implementation of the PsExec protocol for executing commands on remote Windows systems over SMB. This tool connects to a target via SMB, uploads and installs the signed PsExecSvc service binary, then communicates with it over named pipes to run commands interactively or detached.
The tool is built on top of the library go-smb and use it to create the PsExec service, upload the service binary, and to communicate over the named pipes.
Supports NTLM, Kerberos, and NTLM relay authentication. Supports multiple PsExecSvc protocol versions (2.0 through 2.43) and encrypted command channels.
Currently, only a single version of the service binary is included for upload and execution. Using the "auto" mode, the client will attempt to detect an existing PsExec service on the system and reuse it if the version is supported and otherwise fallback to uploading the included version.
I built this tool to facilitate interaction with Windows from Linux and as way to stay a bit more undetected during assessments.
MIT License - see source files for details.
makeProduces a statically linked go-psexec binary for linux/amd64.
Usage: ./go-psexec [options]
options:
--host Hostname or ip address of remote server. Must be hostname when using Kerberos
-P, --port SMB Port (default 445)
-d, --domain Domain name to use for login
-u, --user Username
-p, --pass Password
-n, --no-pass Disable password prompt and send no credentials
--hash Hex encoded NT Hash for user password
--local Authenticate as a local user instead of domain user
--null Attempt null session authentication
-k, --kerberos Use Kerberos authentication. (KRB5CCNAME will be checked on Linux)
--dc-ip Optionally specify ip of KDC when using Kerberos authentication
--target-ip Optionally specify ip of target when using Kerberos authentication
--aes-key Use a hex encoded AES128/256 key for Kerberos authentication
-c, --command Executable to run (default cmd.exe)
--args Arguments to pass to the executable
-s, --system Run command as NT AUTHORITY\\SYSTEM
-w, --workdir Working directory for remote process (default c:\Windows)
--alt-user Username for spawning process with LogonUserW instead of CreateProcessAsUserW.
Requires correct logon rights.
--alt-pass Password for spawning process with LogonUserW
--no-interactive Run command detached and don't wait for it to finish
--desktop Spawn command window on remote console instead of locally. When used
with --alt-user, LOGON_INTERACTIVE, otherwise LOGON_SERVICE
--num <n> Specify which desktop to spawn window on (default Console 0xFFFFFFFF)
-l, --restricted Spawn process with a restriced token, low integrity process
-e, --elevated Spawn process with an elevated token, high integrity process
--no-profile Do not load user profile. Only relevant when using --alt-user
-a, --affinity Specify comma separated list of CPUs where the program can run (default all CPUs)
--priority Specify process priority: background,low,belownormal,normal,abovenormal,high,realtime (default normal)
--service Name of service to create (default PSEXESVC)
--display Display name of service to create (default same as --service name)
--pipe Name of pipe to create (also name of binary created in --service-dir and --share) (default PSEXESVC)
--client-name Client computer name to send (default random name)
--share SMB share to upload service binary to (default ADMIN$)
--share-root Absolute path the share maps to on the remote system (default C:\Windows\)
--service-dir Directory relative to share root for service binary (default "", i.e., share root)
--svc-version <ver> PsExecSvc protocol version: auto, 2.43 (default auto)
--force-pubkey Use RSA pubkey exchange for v2.43 instead of session-key derived AES
--clean Only perform cleanup of service and service binary
--noclean Skip cleanup e.g., service will be left running
--relay Start an SMB listener that will relay incoming
NTLM authentications to the remote server and
use that connection. NOTE that this forces SMB 2.1
without encryption.
--relay-port <port> Listening port for relay (default 445)
--socks-host <target> Establish connection via a SOCKS5 proxy server
--socks-port <port> SOCKS5 proxy port (default 1080)
-t, --timeout <duration> Dial timeout specified in 5s, 1m, 10m format (default 5s)
--dns-host <ip:port> Override system's default DNS resolver
--dns-tcp Force DNS lookups over TCP. Default true when using --socks-host
--noenc Disable smb encryption
--smb2 Force smb 2.1
--verbose Enable verbose logging
--debug Enable debug logging
-v, --version Show version
Connect with domain credentials and get a cmd.exe shell:
./go-psexec --host 192.168.1.10 -d corp -u admin -p 'P@ssw0rd'Connect with administrator credentials and get a cmd.exe shell running as SYSTEM:
./go-psexec --host 192.168.1.10 -d corp -s -u admin -p 'P@ssw0rd'Execute ipconfig /all:
./go-psexec --host 192.168.1.10 -d corp -u admin -p 'P@ssw0rd' \
-c ipconfig --args "/all"./go-psexec --host 192.168.1.10 -d corp -u admin -p 'P@ssw0rd' \
-c powershell.exe --args "-NoProfile -Command Get-Process"Authenticate using an NT hash instead of a password:
./go-psexec --host 192.168.1.10 -d corp -u admin \
--hash <NT Hash> Using a Kerberos ticket from a ccache file (make sure to use FQDN for domain):
KRB5CCNAME=/tmp/krb5cc_admin ./go-psexec --host dc01.corp.local \
-k -n -d corp.local -u admin --dc-ip <ip of domain controller>With an AES256 key directly:
./go-psexec --host dc01.corp.local -k -d corp.local -u admin \
--aes-key 4a3c...hex... --dc-ip <ip of domain controller>Use --alt-user to spawn the process via LogonUserW as a specific user. This requires the user to have the appropriate logon rights on the target:
./go-psexec --host 192.168.1.10 -d corp -u admin -p 'P@ssw0rd' \
--alt-user serviceaccount --alt-pass 'SvcP@ss' \
-c whoamiRun a command without waiting for output:
./go-psexec --host 192.168.1.10 -d corp -u admin -p 'P@ssw0rd' \
--no-interactive -c notepad.exeLaunch a visible window on the remote console:
./go-psexec --host 192.168.1.10 -d corp -u admin -p 'P@ssw0rd' \
--desktop -c calc.exeUse non-default names to avoid conflicts or for stealth:
./go-psexec --host 192.168.1.10 -d corp -u admin -p 'P@ssw0rd' \
--service MYSERVICE --pipe MYSERVICE --display "My Service"Place the service binary on C$ instead of ADMIN$ but keep in mind that
all versions of PsExec from 2.30 use a key file that is always located in ADMIN$:
./go-psexec --host 192.168.1.10 -d corp -u admin -p 'P@ssw0rd' \
--share 'C$' --share-root 'C:\' --service-dir 'ProgramData\'Remove a previously installed service and binary without running anything:
./go-psexec --host 192.168.1.10 -d corp -u admin -p 'P@ssw0rd' --cleanLeave the service running after execution for repeated use:
./go-psexec --host 192.168.1.10 -d corp -u admin -p 'P@ssw0rd' --nocleanOn subsequent runs the tool will detect the running service and reuse it without re-uploading.
The tool ships with the PsExecSvc v2.43 binary embedded as a hex string in servicebinary.go.
The protocol supports versions 2.0 through 2.43, and the tool can communicate with any supported version it finds already installed on a target.
However, to upload a specific version that is supported by the project, the binary must be embedded in the build.
To add support for uploading an additional version:
Convert your PSEXESVC.EXE binary to a Go hex string constant:
xxd -p PSEXESVC.EXE | tr -d '\n' > svc_hex.txtAdd a new hex constant and decode it in the init() function, following the existing pattern:
const psexecsvc2_20_hex = "4d5a9000..." // paste from svc_hex.txt
func init() {
// existing v2.43 decode ...
psexecbytes2_20, _ = hex.DecodeString(psexecsvc2_20_hex)
}Add a global variable alongside the existing one:
var psexecbytes2_20 []byteIn main.go, update two switch statements:
The --svc-version validation (search for Add to this list):
switch svcVersionStr {
case "auto", "2.43", "2.20": // add new version string hereThe upload version selection (search for Determine which binary to upload):
switch svcVersion {
case Version_2_43, 0xFFFFFFFF:
uploadVersion = Version_2_43
case Version_2_20:
uploadVersion = Version_2_20And in service.go, add a case in uploadServiceBinary to write the correct bytes:
case Version_2_20:
err = session.PutFile(shareName, filename, 0, bytes.NewReader(psexecbytes2_20).Read)
detectedVersion = Version_2_20make
./go-psexec --host TARGET -u admin -p pass --svc-version 2.20