Something that I have been working on for the past week or so is building a TCP server that I can use to issue commands from remotely and have it carry out on the remote server.
I’ve already written a PowerShell Chat server and chronicled that build, but this is something a little different. I will not be handling more than one client connection and I will instead be running commands on the remote system based on what I send to the open port.
Before I begin, I need to lay out some requirements that I want to hit if I trust that this will be usable in something other than a lab environment.
- Provide some sort of authentication mechanism
- Also want to impersonate remote client token to run commands
- Capable of returning actual objects from remote system (serialization)
- Handle a single connection and then close connection after the command issued and output returned to client
With that in mind, I can now start out by initializing my server. Note that I will be jumping back and forth between a Client and Server with my code examples. I am also using two separate systems to show a more realistic approach as to how this works.
Serialization of Data
Before I dive into the TCP side of the house, I want to quickly cover what how we are going to receive the output data from the remote system over the network. If you work with PowerShell remoting at all, you know that serialization plays a major role in receiving the data from a remote system.
In PowerShell V3+, we have the [System.Management.Automation.PSSerializer] class publicly available to us and the appropriate Serialize() and Deserialize() methods available to us to transform the data into XML prior to shipping across the network.
$data = Get-ChildItem -File | Select -First 1 -Property FullName, Length, LastWriteTime $serialized = [System.Management.Automation.PSSerializer]::Serialize($Data) $serialized
$deserialized = [System.Management.Automation.PSSerializer]::Deserialize($serialized) $deserialized
Of course, there are distinct possibilities that systems are still running PowerShell V2 in which case these are not publicly available. Using Reflection, we can still access the methods to serialize and deserialize the data. Fortunately, the PowerShell Community has done the work for us and has two functions (ConvertTo-CliXml (Original Author Oisin Grehan <Twitter | Blog>; Current Version Joel Bennett <Twitter | Blog> ) and ConvertFrom-CliXml (Original Author David Sjstrand; Current Version Joel Bennett)) readily available to use. The remote server that I am using for my demo only has PowerShell V2 on it, so I will be making use of ConvertTo-CliXml.
Server
First it is time to initialize the port listener on the server so we can start accepting connections.
First let’s check to see if the port is opened.
Nothing opened, now lets open that port up.
##Server [console]::Title = ("Server: $env:Computername <{0}> on $port" -f ` [net.dns]::GetHostAddresses($env:Computername))[0].IPAddressToString $port=1655 $endpoint = new-object System.Net.IPEndPoint ([system.net.ipaddress]::any, $port) $listener = new-object System.Net.Sockets.TcpListener $endpoint $listener.start() $client = $listener.AcceptTcpClient()
This is a blocking method meaning that until something makes a connection, this will prevent me from accessing the console.
Now that we are listening, lets make an initial connection from the client side.
Client
##Client [console]::Title = ("Server: $env:Computername <{0}>" -f ` [net.dns]::GetHostAddresses($env:Computername))[0].IPAddressToString $port=1655 $server='Boe-PC' $client = New-Object System.Net.Sockets.TcpClient $server, $port
Connection has been made. We can verify on the server by seeing if the console has opened up and also by running another netstat.
So what happens now? Here is where we make the decision to use a NegotiateStream vs. a regular network stream. By using a NegotiateStream, we will be able to then provide an Authentication Stream that will be used to transfer client and server authentication data between the two as well as being able to sign and encrypt the data transmission. By using a network stream, anonymous users could easily connect to the remote system and issue commands as the user that started the listener! Not exactly what you want to deal with.
$stream = $client.GetStream() $NegotiateStream = New-Object net.security.NegotiateStream -ArgumentList $stream
First I get the network stream by calling the GetStream() method and then use that in the construction of the NegotiateStream object.
Before I do anything else, I need to kick off the same stream on the server.
Server
$stream = $client.GetStream() $NegotiateStream = New-Object net.security.NegotiateStream -ArgumentList $stream #Validate Alternate credentials Try { $NegotiateStream.AuthenticateAsServer( [System.Net.CredentialCache]::DefaultNetworkCredentials, [System.Net.Security.ProtectionLevel]::EncryptAndSign, [System.Security.Principal.TokenImpersonationLevel]::Impersonation ) Write-host "$($client.client.RemoteEndPoint.Address) authenticated as $($NegotiateStream.RemoteIdentity.Name) via $($NegotiateStream.RemoteIdentity.AuthenticationType)" -Foreground Green -Background Black } Catch { Write-Warning $_.Exception.Message }
Same as with the client, I have to construct the NegotiateStream object using the network stream. After that it is time to accept an authentication request from the client.
There are 4 possible parameter sets with the AuthenticateAsServer() method. In this instance, I am choosing to supply a set of default credentials, making sure that I encrypt and sign the data being transferred and declaring that I will only accept Impersonation as my token impersonation level. Attempts at negotiating anything else will end up with a denied connection.
Once I call this method, it becomes a blocking call until I attempt authentication from my client.
Client
I am now going to use AuthenticateAsClient() to try to pass some alternate credentials (proxb) to the server in hopes of being let in.
Try { $NegotiateStream.AuthenticateAsClient( (Get-Credential).GetNetworkCredential(), 'MYSERVICE\boe-pc', [System.Net.Security.ProtectionLevel]::EncryptAndSign, [System.Security.Principal.TokenImpersonationLevel]::Impersonation ) } Catch { Write-Warning $_.Exception.Message }
The AuthenticateAsClient() method is similar to what we used with the Server. It has multiple parameter sets including one where you supply no parameters. Because I want to make sure to negotiate Impersonation with the server, I am making sure to supply my default network credential, a SPN that I made up, EncryptAndSign so the data is transmitted securely and finally my TokenImpersonationLevel as Impersonate.
Inspecting the NegotateStream object, you can see that IsEncrypted and IsSigned is True which is what we wanted as well as the ImpersontationLevel is set to Impersonate. Diving deeper into the object via the RemoteIdentity property, we can see that we are using the SPN that we created as well as the AuthentcationType, in this case NTLM.
Server
On the server side, we will also inspect both the NegotiateStream and the RemoteIdentity property as well to see what they look like.
You can see by the Write-Host that I included that the remote client successfully connected as Boe-PC\proxb and that the authentication type was NTLM.
Same information on the NegotiateStream as what was one the client. But the RemoteIdentity property is much different. Here we can see the remote user’s name, the token being used as well as the group memberships (shown as a SID) that this user belongs to in relation to this server.
At this point, I could check to see if the remote user has access and make a decision to allow the connection or not. Something like this could be done:
([Security.Principal.WindowsPrincipal]$NegotiateStream.RemoteIdentity).IsInRole('Administrators')
From here I could make a decision based on whether it comes back as True or False to continue allowing the connection or halt it. In this case, I would allow it to continue on.
So now that we have a good connection along with proper authentication, we can continue on with the connection and begin with attempting to impersonate the remote client.
Lets check out who the current user is on the server via the TCP server.
[System.Security.Principal.WindowsIdentity]::GetCurrent()
As you can see, I am currently running as my Administrator account (smart, right?) that launched the server. Now I will attempt to impersonate the remote client (proxb) and see what happens.
$remoteUserToken = $NegotiateStream.RemoteIdentity.Impersonate()
I saved the output object (System.Security.Principal.WindowsImpersonationContext) as a variable because this will be invaluable later on when I need to stop impersonating the remote client.
[System.Security.Principal.WindowsIdentity]::GetCurrent()
As you can see, I am now running under the boe-pc\proxb account after a successful impersonation! From here, depending on the rights that you have on the system, you can run commands as the user that you impersonated. Of course, if you have little to no rights, it will be quite difficult to do much of anything.
Client
Now that we have accomplished the connection and impersonation on the server, I will issue a command that will be run on the remote system and return the results back to the client.
$data = [text.Encoding]::Ascii.GetBytes('Get-WMIObject Win32_OperatingSystem | Select __Server, Caption') Write-Verbose "Sending $($Data.count) bytes" $NegotiateStream.Write($data,0,$data.length) $NegotiateStream.Flush()
All that I’ve done is taken the command as a string and converted into bytes using the GetBytes() method before sending across the network via the NegotiateStream over to the remote system.
Server
First I will check to see if any data is available:
$Stream.DataAvailable
I know that there is data available so now I need to begin work to get the data and convert it back into something usable.
$stringBuilder = New-Object Text.StringBuilder Do { [byte[]]$byte = New-Object byte[] 1024 Write-Verbose ("{0} Bytes Left" -f $client.Available) $bytesReceived = $NegotiateStream.Read($byte, 0, $byte.Length) If ($bytesReceived -gt 0) { Write-Verbose ("{0} Bytes received" -f $bytesReceived) [void]$stringBuilder.Append([text.Encoding]::Ascii.GetString($byte[0..($bytesReceived - 1)])) } Else { $activeConnection = $False Break } } While ($Stream.DataAvailable)
I use the StringBuilder class to handle the reconstruction of the command and continue with a Do loop until all of the data has been received from the remote client. You might be asking why 82 bytes were sent and only 62 bytes are shown to have been received. This is due to the encryption that is being applied prior to sending the data across the network.
With the StringBuilder object, we have to cast it out to a string to see the data inside of it.
$stringBuilder.ToString()
There is our command that was sent from the remote client all ready for us to run here on the remote system. Speaking of which, lets continue on and run this command and save the results.
$string = $stringBuilder.ToString() Write-Verbose ("Message received from {0} on {1}:`n{2}" -f $client.client.RemoteEndPoint.Address, ([System.Security.Principal.WindowsIdentity]::GetCurrent()).Name, $string) Try { $ErrorActionPreference = 'Stop' $Data = [scriptblock]::Create($string).Invoke() } Catch { $Data = $_.Exception.Message } If (-Not $Data) { $Data = 'No data to return!' } Try { $ErrorActionPreference = 'stop' $serialized = [System.Management.Automation.PSSerializer]::Serialize($Data) } Catch { $serialized = $data | ConvertTo-CliXml }
I use [scriptblock]::Create() to build out the command and then use Invoke() to run the command. It’s really not much better than using Invoke-Expression. With either of these, you need to be mindful of possible code injections.
From there, I need to serialize the object that is being returned so I can send it over to the remote client. First I try the Serialize() method and if that fails (I temporarily set the $erroractionpreference to Stop to make sure the error is terminating), then default to the ConvertTo-CliXml function. The resulting data prior to being sent looks like this:
Next up is to convert that data into bytes and send it back to the client.
$ErrorActionPreference = 'Continue' #Resend the Data back to the client $bytes = [text.Encoding]::Ascii.GetBytes($serialized) #Send the data back to the client Write-Verbose ("Sending {0} bytes" -f $bytes.count) -Verbose $NegotiateStream.Write($bytes,0,$bytes.length) $NegotiateStream.Flush()
Client
I am basically going to repeat the same process on the client that I did on the server in checking for any data and then pulling it down as bytes and converting it to a string.
$stringBuilder = New-Object Text.StringBuilder While ($client.available -gt 0) { Write-Verbose "Processing Bytes: $($client.Available)" -Verbose #$clientstream = $TcpClient.GetStream() [byte[]]$inStream = New-Object byte[] $client.Available $buffSize = $client.Available $return = $NegotiateStream.Read($inStream, 0, $buffSize) [void]$stringBuilder.Append([System.Text.Encoding]::ASCII.GetString($inStream[0..($return-1)])) }
Let’s verify that the data came across.
Now we need to deserialize the data so it is an actual object (PowerShell loves objects! ).
Try { $ErrorActionPreference = 'stop' $deserialized = [System.Management.Automation.PSSerializer]::DeSerialize($stringbuilder.ToString()) } Catch { $deserialized = $stringbuilder.ToString() | ConvertFrom-CliXml } $ErrorActionPreference = 'Continue'
Now we can look at the finished product.
Bear in mind that this is a deserialized object, so you will only have properties and a few select methods such as ToString() available.
Now that I am done with my connection, I need to clean up after myself.
Client
$stream.Close() $client.Close() $NegotiateStream.Close() $stream.Dispose() $client.Dispose() $NegotiateStream.Dispose()
Server
$listener.Stop() $reader.Dispose() $stream.Dispose() $client.Dispose() $NegotiateStream.Dispose() $remoteUserToken.Undo() $remoteUserToken.Dispose()
A More Functional Way to Do This
Working with either of these (client or server) requires a lot of monitoring and manual commands to make sure that you are tracking what is being sent and received between these two connections. Fortunately, I have written a couple of functions that will make this much easier to manage.
First we need to load the module up.
Import-Module .\TCPServer.psm1 -Verbose
Invoke-TCPServer
The first function is called Invoke-TCPServer which can be used to either start a TCP server on a local or remote system. You can specify a Computername, Port and a Credential to run the TCP server as (also useful with remote systems that you are starting the server on).
This will start the TCP server and handle one connection at a time as well as using the negotiate stream that I have discussed here. After each connection and command ran based on the client, it will drop the client connection and force another reconnect.
An object is returned when you use the function showing the Computername, Port, ProcessID of the local/remote process being used for the server as well as a port check attempt (an initial warning will display on the server window because it will try to authenticate against the port check; this can be ignored) that you can then reference to track the TCP server.
Invoke-TCPServer -Computername 'boe-pc' -Port 1655 ` -Credential 'boe-pc\proxb' -Verbose
The IsPortAvailable is kind of hit or miss (I may pull this property in the next release), but the TCP server is now up and running on the remote system. I can verify on the remote system just to be sure.
For the sake of demonstrating and showing the window, I am going to run the server locally so the window is visible.
Again, the Warning is more of a friendly warning due to the port check that occurs after starting the process.
Send-Command
This now leads into my second function, called Send-Command. This does exactly what the function says, sends a command to the remote system and then waits for a response and displays the response (usually an object) to the client console.
Using my currently running server, lets send a simple command to it and see what happens!
Send-Command -Computername 'boe-pc' -Port 1655 ` -Command 'Get-WMIObject Win32_OperatingSystem | Select __Server, Caption' ` -Verbose -Credential 'boe-pc\proxb'
Works rather well. On the client piece, we can see that it attempts authentication and then sends its data to the remote server and waits for a response. Once the response has been received, it presents the data on the console. Now let’s look at the server side.
As you can see, the server took the command, ran it and sent the returned object back to the client. After sending the data back to the client, the connection to the client is closed.
The download link for these two functions are below. Note that these are still Proof of Concept and while they seem like a secure method, it should still be used with caution, especially if used in production. I have plans for more things to add to this, but for now, this is what it is. Feel free to let me know of any bugs or things that you would like to see added.
Download TCP Server Module
I’m having trouble getting past the authenticateasclient part. Can you help me? I have tried multiple ways and it hasn’t worked yet. Thanks
This works if I paste it in a powershell window, but it fails when I run the server side within a .ps1 script file.
This code is great, but only useful to me if I could run it within a .ps1 script.
When running in .ps1 script on the server side, it fails after receiving a connection from the client for this command:
$client = $listener.AcceptTcpClient()
The error is:
Cannot convert value “System.Net.Sockets.TcpClient” to type “System.Management.Automation.SwitchParameter”. Boolean parameters accept only Boolean values and numbers, such as $True, $False, 1 or 0
Is there a way around this error?
To repro, save this to startserver.ps1
$Port=2222
$endpoint = new-object System.Net.IPEndPoint ([system.net.ipaddress]::any, $Port)
$listener = new-object System.Net.Sockets.TcpListener $endpoint
$listener.start()
$client = $listener.AcceptTcpClient()
Then run the script and on the same computer, run this in a powershell window:
$Port=2222
$ServerName=$Env:COMPUTERNAME
$client = New-Object System.Net.Sockets.TcpClient $ServerName, $Port
This is when the server side will produce an error message. I tried on Win 2012 R2 and Win10. I also tried with Powershell 4 and 5.
Hey mate,
so I am testing bit by bit I know you have created it but i need to work it out 🙂
so running these bits by bits works
If i want many people to ‘login’ and chat how do i do this ? i can only get this so far to do a 1 to 1 chat
and also $NegotiateStream does this work by connecting to the domain and verify they exist ?
Pav.
###############################
Run this part first
#
$port=8008
$endpoint = new-object System.Net.IPEndPoint ([system.net.ipaddress]::any, $port)
$listener = new-object System.Net.Sockets.TcpListener $endpoint
$listener.start()
$client = $listener.AcceptTcpClient()
$stream = $client.GetStream()
#
Check if data has been sent
#
$Stream.DataAvailable
#
form the data to readable text
#
$stringBuilder = New-Object Text.StringBuilder
Do {
[byte[]]$byte = New-Object byte[] 1024
Write-Verbose (“{0} Bytes Left” -f $client.Available)
$bytesReceived = $Stream.Read($byte, 0, $byte.Length)
If ($bytesReceived -gt 0) {
Write-Verbose (“{0} Bytes received” -f $bytesReceived)
[void]$stringBuilder.Append([text.Encoding]::Ascii.GetString($byte[0..($bytesReceived – 1)]))
} Else {
$activeConnection = $False
Break
}
} While ($Stream.DataAvailable)
$stringBuilder.ToString()
#
Send a message
#
$MessageToSend = “meow”
$Data = [text.Encoding]::Ascii.GetBytes($MessageToSend)
Write-Host “Sending $($Data.count) bytes”
$Stream.Write($Data,0,$Data.length)
$Stream.Flush()
#
run this when finished to close ports
#
$listener.Stop()
$stream.Close()
$client.Close()
$stream.Dispose()
$client.Dispose()
Cool stuff indeed!
I’m looking for a way to make the server listen to a range of ports (about 100 ports) and forward all traffic to a different ip:port once a connection is made. Do you think is possible?
I’m looking for critique/alternate suggestions on this line of thought:
I want Junior, a non-admin user, to reboot a specified list of computers. Ideally, GPO on each target computer could be set up to allow for this, but getting that set up will take quite some time, and I want a simple safe interim solution. I read this post and think what if:
1. Junior authenticates to my tcp server, then
2. Junior chooses from a list of servers I present,
3a. my tcp server sends heads-up email plus reboot command to the servers Junior chose. This is simple but sort of unconfortable because I have a tcp server run by a privileged id.
3b. Or maybe the server runs as a non-privileged account which is able to send reboot request info to a privileged account that actually does the reboots.
Interesting question. I think PowerShell remoting would be better suited for a situation such as this. I say that because you can leverage a custom console session that can limit the number of commands available to your user or build out a proxy function that utilizes the restart-computer and send-mailmessage cmdlets into a single function. You could even have the PSSession run as an administrator account with the necessary rights to perform the actions and have the ACLs in place to make sure only specific accounts can access the session.
That being said, The TCP Server would let the user log onto it, and assuming that the impersonation level is set to ‘Delegate’ (this would be required if you are attempting to cross over to other servers from the remote system; haven’t tested with ‘Impersonation’ level), the user would still have to have the necessary rights on the server to issue the commands to the other systems regardless of the rights that the user that runs the TCP server. If you wanted the privileged account to run the commands, then you would have to have some sort of check for specific accounts to allow access to run the required commands ‘as the privileged account’.
Hope this helps! Let me know if you have any more questions or need more info on what I have mentioned.
Funny it sounds like you’re about to fix the shortcomings in this: http://gallery.technet.microsoft.com/2d191bcd-3308-4edd-9de2-88dff796b0bc
The Windows update module From Michal Gajda.
This is really cool stuff!