C# multi-hop SSH (SSH through SSH) - c#

I wrote a C# console program to connect from A (Windows 10, Console C# app) over SSH to B (Linux server) and from there on to C (Linux server), but I cannot connect from B to C (from A to B is ok).
When I connect from A over Windows terminal to B and from B's terminal to C, it works, so I proved that my credentials are fine.
I am using Renci.SshNet for C#
I created a class Server with a .Connect(), .Disconnect() and .Execute() extension methods and then the two class instances Broker and Destination
My code looks like:
if (Broker.Connect())
{
Broker.Execute("pwd");
if (Destination.Connect())
{
Destination.Execute("pwd");
Destination.Disconnect();
}
Broker.Disconnect();
}
The Ssh connection objets are created like var broker = new SftpClient("Ip", Port, "User", "Pass")
Then I am internally using broker.Connect() and broker.Disconnect() in Renci.Ssh.Net lib given methods
To broker.Execute("cmd") I basically do
var output = host.Ssh.RunCommand(str);
var res0 = output.ExitStatus;
var res1 = output.Result;
var res2 = output.Error;
My code works for the first part as I manage to get the output of Broker.Execute("pwd") but it does not connect on Destination.Connect() returning the message A connection attempt failed because the connected party did not properly respond after a period of time, or established connection failed because connected host has failed to respond
My aim ist to multi-hop using an automated process from within C#: users must not interact with any console and I cannot modify nor store any files on the Linux sites
Any idea where the problem lies?
Thanks in advance,

I will like to summarize here how I ended solving this issue with the help of some valuable hints gathered over the net and from #jeb:
Open a cmd.exe console, type ssh userC#hostC -p portC-J userB#hostB - p portB (portB and portC can be ommited if they are the default port 22) and then you will be promped to enter passwordB and passwordC - in this order.
If the connection succeeded you will be then on the hostC console and will manage to do whatever you intend to do.
The code you'll need is:
static void RunSshHop(params string[] cmds)
{
try
{
using (Process p = new Process())
{
p.StartInfo = new ProcessStartInfo("cmd.exe")
{
RedirectStandardInput = true,
UseShellExecute = false,
//WorkingDirectory = #"d:\" // dir of your "cmd.exe"
};
p.OutputDataReceived += p_DataReceived;
p.ErrorDataReceived += p_DataReceived;
p.Start();
// way 1: works
foreach (var e in cmds)
p.StandardInput.Write($"{e}\n"); // cannot use 'WriteLine' because Windows is '\r' and Linux is '\n'
/* way 2: works as well
using (var sw = p.StandardInput)
{
foreach (var e in cmds)
if (sw.BaseStream.CanWrite)
sw.Write($"{e}\n"); // cannot use 'WriteLine' because Windows is '\r' and Linux is '\n'
}
//*/
p.WaitForExit();
if (p.HasExited)
{
Console.WriteLine($"ExitCode: {p.ExitCode}");
}
}
}
catch (Exception ex)
{
Console.WriteLine(ex);
}
}
And you can call it like this:
static void Main(string[] args)
{
RunSshHop(
"ssh userC#hostC -p portC-J userB#hostB - p portB",
"pwd",
//"...",
"ls"
);
}
To avoid having to enter the passwords for each host, you can also create an SSH key pair like this:
open cmd.exe console
type ssh-keygen -t rsa
choose path where to save the public and private keys that are to be generated (press enter to use the default destination)
copy the destination, you will need it later to get back yxour keys :-)
to manage an automated process, you have to leave the passphrase empty
-once the keys are generated, log onto the first host over ssh like ssh youruser#firsthost -p hostport (the -p hostport part can be ignored if port is the default 22)
type ssh-copy-id youruser#firsthost -p hostport
accept
repeat the process for the second host

Related

How can I programmatically turn off or on 'Windows Features'

When I try to update Windows features; When I update UseShellExecute to "true"; "The Process object must have the UseShellExecute property set to false in order to redirect IO streams." I get an error. When I set it to False; Unable to update. How can I do it ? Do you have any other suggestions?
static void InstallIISSetupFeature()
{
var featureNames = new List<string>() {
"IIS-WebServerRole",
"IIS-WebServer",
"IIS-CommonHttpFeatures",
"IIS-HttpErrors",
"IIS-HttpRedirect",
"IIS-ApplicationDevelopment",
"IIS-Security",
"IIS-RequestFiltering",
"IIS-NetFxExtensibility",
"IIS-NetFxExtensibility45",
"IIS-HealthAndDiagnostics",
"IIS-HttpLogging",
"IIS-LoggingLibraries",
"IIS-RequestMonitor",
"IIS-HttpTracing",
"IIS-URLAuthorization",
"IIS-IPSecurity",
"IIS-Performance",
"IIS-HttpCompressionDynamic",
"IIS-WebServerManagementTools",
"IIS-ManagementScriptingTools",
"IIS-IIS6ManagementCompatibility",
"IIS-Metabase",
"IIS-HostableWebCore","IIS-StaticContent",
"IIS-DefaultDocument",
"IIS-DirectoryBrowsing",
"IIS-WebDAV",
"IIS-WebSockets",
"IIS-ApplicationInit",
"IIS-ASPNET",
"IIS-ASPNET45",
"IIS-ASP",
"IIS-CGI",
"IIS-ISAPIExtensions",
"IIS-ISAPIFilter",
"IIS-ServerSideIncludes",
"IIS-CustomLogging",
"IIS-BasicAuthentication",
"IIS-HttpCompressionStatic",
"IIS-ManagementConsole",
"IIS-ManagementService",
"IIS-WMICompatibility",
"IIS-LegacyScripts",
"IIS-LegacySnapIn",
"IIS-FTPServer",
"IIS-FTPSvc",
"IIS-FTPExtensibility",
"IIS-CertProvider",
"IIS-WindowsAuthentication",
"IIS-DigestAuthentication",
"IIS-ClientCertificateMappingAuthentication",
"IIS-IISCertificateMappingAuthentication",
"IIS-ODBCLogging",
"NetFx4-AdvSrvs",
"NetFx4Extended-ASPNET45",
"NetFx3",
"WAS-WindowsActivationService",
"WCF-HTTP-Activation",
"WCF-HTTP-Activation45",
"WCF-MSMQ-Activation45",
"WCF-NonHTTP-Activation",
"WCF-Pipe-Activation45",
"WCF-TCP-Activation45",
"WCF-TCP-PortSharing45",
"WCF-Services45",
};
ManagementObjectSearcher obj = new ManagementObjectSearcher("select * from Win32_OperatingSystem");
foreach (ManagementObject wmi in obj.Get())
{
string Name = wmi.GetPropertyValue("Caption").ToString();
Name = Regex.Replace(Name.ToString(), "[^A-Za-z0-9 ]", "");
if (Name.Contains("Server 2008 R2") || Name.Contains("Windows 7"))
{
featureNames.Add("IIS-ASPNET");
featureNames.Add("IIS-NetFxExtensibility");
featureNames.Add("WCF-HTTP-Activation");
featureNames.Add("WCF-MSMQ-Activation");
featureNames.Add("WCF-Pipe-Activation");
featureNames.Add("WCF-TCP-Activation");
featureNames.Add("WCF-TCP-Activation");
}
string Version = (string)wmi["Version"];
string Architecture = (string)wmi["OSArchitecture"];
}
foreach (var featureName in featureNames)
{
Run(string.Format("dism/online/Enable-Feature:{0}", featureName));
}
}
static void Run(string arguments)
{
try
{
string systemPath = Path.Combine(Environment.ExpandEnvironmentVariables("%windir%"), "system32");
var dism = new Process();
dism.StartInfo.WorkingDirectory = systemPath;
dism.StartInfo.Arguments = arguments;
dism.StartInfo.FileName = "dism.exe";
dism.StartInfo.Verb = "runas";
dism.StartInfo.UseShellExecute = true;
dism.StartInfo.RedirectStandardOutput = true;
dism.Start();
var result = dism.StandardOutput.ReadToEnd();
dism.WaitForExit();
}
catch (Exception ex)
{
}
}`
I tried to update the feature with dism.exe and cmd.exe, when it gave an authorization error, I used the Verb property
`
Since the use of .Verb = "RunAs" requires .UseShellExecute = true, and since the latter cannot be combined with RedirectStandardOutput = true, you cannot directly capture the elevated process' output in memory.
It seems that the system itself, by security-minded design, prevents a non-elevated process from directly capturing an elevated process' output.
The workaround is to launch the target executable (dism.exe, in your case) indirectly, via a shell, and then use the latter's redirection feature (>) to capture the target executable's output (invariably) in a file, as shown below.
string systemPath = Path.Combine(Environment.ExpandEnvironmentVariables("%windir%"), "system32");
// Create a temp. file to capture the elevated process' output in.
string tempOutFile = Path.GetTempFileName();
var dism = new Process();
dism.StartInfo.WorkingDirectory = systemPath;
// Use cmd.exe as the executable, and pass it a command line via /c
dism.StartInfo.FileName = "cmd.exe" ;
// Use a ">" redirection to capture the elevated process' output.
// Use "2> ..." to also capture *stderr* output.
// Append "2>&1" to capture *both* stdout and stderr in the file targeted with ">"
dism.StartInfo.Arguments =
String.Format(
"/c {0} {1} > \"{2}\"",
"dism.exe", arguments, tempOutFile
);
dism.StartInfo.Verb = "RunAs";
dism.StartInfo.UseShellExecute = true;
dism.Start();
dism.WaitForExit();
// Read the temp. file in which the output was captured...
var result = File.ReadAllText(tempOutFile);
// ... and delete it.
File.Delete(tempOutFile);
First, you can use WindowsPrincipal::IsInRole() to check if you're running elevated.
See Microsoft Learn for details.
Second, this may be one of those cases where using native PS is easier than the cmdlet approach (admittedly, still not great).
If the script is supposed to run on clients as well as server operating systems: use Get-WmiObject or Get-CimInstance to get a reference to what you're running on. ActiveDirectory also has that information (in operatingSystem attribute).
For servers use Get-WindowsFeature in ServerManager module.
For clients use Get-WindowsOptionalFeature with switch -Online in DISM module which, if you indeed need to support OSes older than 6.3.xxxx, can be copied over from a machine that has it and added to $Env:Path before C:\Windows and C:\Windows\System32.
For either platform just pass the list of features to configure.
If in a (binary) cmdlet you have to call external tools then the advantage of them is mostly gone. It may be possible to access Windows CBS using a managed API to avoid this but even then the script based approach gets more results faster, especially since you can just just put together a quick wrapper around dism.exe .

Program to downloading a file from S3 to remote EC2 Instance of Windows

I am writing a program in C# (dotnet 6/Mac) which will issue a PowerShell command to a remote EC2 instance running Windows (2012/PowerShell version 5.x) to download a file from S3.
I am on a Mac and I am able to connect to the EC2 Instance with PowerShell for Mac.
Here is the C# program:
public void DownloadS3FileToRemoteMachine(string host,
string user,
string password,
string bucket,
string s3path,
string localPath)
{
string s3DownloadCommand =
$"aws s3 cp s3://{bucket}{s3path} {localPath}";
var securePass = new SecureString();
foreach (char p in password)
{
securePass.AppendChar(p);
}
var credential = new PSCredential(user, securePass);
var connectionInfo = new WSManConnectionInfo
{
ComputerName = host,
Credential = credential,
NoEncryption = true,
Scheme = WSManConnectionInfo.HttpScheme
};
using Runspace rs = RunspaceFactory.CreateRunspace(connectionInfo);
using PowerShell? ps = PowerShell.Create(rs).AddScript(s3DownloadCommand);
Collection<PSObject>? results;
try
{
rs.Open();
results = ps.Invoke();
if (ps.HadErrors)
{
string errors = string.Join(Environment.NewLine, ps.Streams
.Error
.ReadAll()
.Select(err => err.ErrorDetails.Message));
_logger.LogError("Error while downloading the file from S3 to local path {LocalPath}, " +
"error {ErrorMsg}", localPath, errors);
}
}
catch (Exception e)
{
_logger.LogError(e, "Error copying the file from S3 to remote machine");
throw;
}
string enumerable = string.Join("\n", results.Select(r => r.ToString()));
_logger.LogInformation(enumerable);
}
With this, I get the error:
Connecting to remote server 10.62.166.198 failed with the following error message : Authorization failed For more information, see the about_Remote_Troubleshooting Help topic.
However, I know that the code works because I have access to another Windows machine running Window 10 on my local network and I am able to successfully download the file on that machine.
If I remove the line NoEncryption = true from the configuration then I get a different message:
Connecting to remote server 10.62.166.198 failed with the following error message : MI_RESULT_FAILED For more information, see the about_Remote_Troubleshooting Help topic.
Any help will be tremendously appreciated.
Thanks to the amazing people on the PowerShell Discord channel (access via https://www.reddit.com/r/PowerShell) I learnt that the current functionality of PowerShell to remote from Linux to Windows is very limited.
I had to choose OpenSSH option i.e. install OpenSSH on Windows machine, thereafter, I was able to access the service via C# like so:
var info = new ConnectionInfo(host, user, new PasswordAuthenticationMethod(user, password));
using var client = new SshClient(info);
client.HostKeyReceived += (_, e) => e.CanTrust = true;
client.Connect();
string s3DownloadCommand = $"aws s3 cp s3://{bucket}/{source} {destination}";
using SshCommand? cmd = client.CreateCommand(s3DownloadCommand);
string? output = cmd.Execute();

Timeout in socket connection in UWP

I have this code which works fine for project type of Console App (.NET Core).
class Program
{
static void Main(string[] args)
{
var L = new TcpListener(IPAddress.Any, 4994);
L.Start();
using (var C = L.AcceptTcpClientAsync().Result)
{
var S = C.GetStream();
var BR = new BinaryReader(S);
var BW = new BinaryWriter(S);
BW.Write("This is from Console!!!");
Console.WriteLine(BR.ReadString());
}
}
}
But when I use this code in project type of Blank App (Universal Windows) like this:
public MainPage()
{
InitializeComponent();
ThreadPool.RunAsync(foo);
}
static void foo(IAsyncAction operation)
{
var L = new TcpListener(IPAddress.Any, 4994);
L.Start();
using (var C = L.AcceptTcpClientAsync().Result)
{
var S = C.GetStream();
var BR = new BinaryReader(S);
var BW = new BinaryWriter(S);
BW.Write("This is from UWP!!!");
Debug.Write(BR.ReadString());
}
}
It will listen to that port when I check it by netstat but when the client wants to connect this exception will be thrown.
System.Net.Sockets.SocketException: 'A connection attempt failed because the connected party did not properly respond after a period of time, or established connection failed because connected host has failed to respond'
The UWP App has Private Networks (Client & Server) and Internet (Client & Server) capabilities.
Turning firewall on and off didn't help.
Target Version: Windows 10 Creators Update (10.0; Build 15063)
Client Code which is a WPF application:
using (var C = new TcpClient("127.0.0.1", 4994))
{
var S = C.GetStream();
var BR = new BinaryReader(S);
var BW = new BinaryWriter(S);
BW.Write("This is a test");
MessageBox.Show(BR.ReadString());
}
Debugging UWP & TCP listeners from localhost has always been problematic. Your code is OK and it should work if you try to connect into it from an external computer. The issue you're seeing is quite likely a bug/hyper-v issue/networking problem in the network isolation.
You can check if the network isolation for your app is enabled (it is by default) running the following from command prompt:
CheckNetIsolation.exe LoopbackExempt -s
My recommendation is to use an external computer to make sure that your code is fine (it should be). After that you can try to fight with the network isolation but that can be frustrating.
Here's an another issue where this has been discussed: Unable to access TCP Server inside a Windows Universal Application

Hostname scanning in C#

Iv'e recently started a new job as an ICT Technician and im creating an Console application which will consists of stuff that will help our daily tools!
My first tool is a Network Scanner, Our system currently runs on Vanilla and Asset tags but the only way we can find the hostname / ip address is by going into the Windows Console tools and nslookup which to me can be improved
I want to create an application in which I enter a 6 digit number and the application will search the whole DNS for a possible match!
Our hostsnames are like so
ICTLN-D006609-edw.srv.internal the d 006609 would be the asset tag for that computer.
I wish to enter that into the Console Application and it search through every hostname and the ones that contain the entered asset tag within the string will be returned along with an ip and full computer name ready for VNC / Remote Desktop.
Firstly how would I go about building this, shall i start the project of as a console app or a WPF. can you provide an example of how I can scan the hostnames via C#, or if there's an opensource C# version can you provide a link.
Any information would be a great help as it will take out alot of issues in the workpalce as we have to ask the customer to go into there My Computer adn properties etc and then read the Computer name back to use which I find pointless.
Regards.
Updates:
*1 C# Version I made: http://pastebin.com/wBWxyyuh
I would actually go about this with PowerShell, since automating tasks is kinda its thing. In fact, here's a PowerShell script to list out all computers visible on the network. This is easily translatable into C# if you really want it there instead.
function Find-Computer( [string]$assetTag ) {
$searcher = New-Object System.DirectoryServices.DirectorySearcher;
$searcher.SearchRoot = New-Object System.DirectoryServices.DirectoryEntry;
$searcher.SearchScope = 'Subtree';
$searcher.PageSize = 1000;
$searcher.Filter = '(objectCategory=computer)';
$results = $searcher.FindAll();
$computers = #();
foreach($result in $results) {
$computers += $result.GetDirectoryEntry();
}
$results.Dispose(); #Explicitly needed to free resources.
$computers |? { $_.Name -match $assetTag }
}
Here's a way you can accomplish this, although it's not the best. You might consider hitting Active Directory to find the legitimate machines on your network. The code below shows how you might resolve a machine name, and shows how to ping it:
static void Main()
{
for (int index = 0; index < 999999; index++)
{
string computerName = string.Format("ICTLN-D{0:000000}-edw.srv.internal", index);
string fqdn = computerName;
try
{
fqdn = Dns.GetHostEntry(computerName).HostName;
}
catch (SocketException exception)
{
Console.WriteLine(">>Computer not found: " + computerName + " - " + exception.Message);
}
using (Ping ping = new Ping())
{
PingReply reply = ping.Send(fqdn);
if (reply.Status == IPStatus.Success)
{
Console.WriteLine(">>Computer is alive: " + computerName);
}
else
{
Console.WriteLine(">>Computer did not respond to ping: " + computerName);
}
}
}
}
Hope that helps...

Any way to turn the "internet off" in windows using c#?

I am looking for pointers towards APIs in c# that will allow me to control my Internet connection by turning the connection on and off.
I want to write a little console app that will allow me to turn my access on and off , allowing for productivity to skyrocket :) (as well as learning something in the process)
Thanks !!
If you're using Windows Vista you can use the built-in firewall to block any internet access.
The following code creates a firewall rule that blocks any outgoing connections on all of your network adapters:
using NetFwTypeLib; // Located in FirewallAPI.dll
...
INetFwRule firewallRule = (INetFwRule)Activator.CreateInstance(
Type.GetTypeFromProgID("HNetCfg.FWRule"));
firewallRule.Action = NET_FW_ACTION_.NET_FW_ACTION_BLOCK;
firewallRule.Description = "Used to block all internet access.";
firewallRule.Direction = NET_FW_RULE_DIRECTION_.NET_FW_RULE_DIR_OUT;
firewallRule.Enabled = true;
firewallRule.InterfaceTypes = "All";
firewallRule.Name = "Block Internet";
INetFwPolicy2 firewallPolicy = (INetFwPolicy2)Activator.CreateInstance(
Type.GetTypeFromProgID("HNetCfg.FwPolicy2"));
firewallPolicy.Rules.Add(firewallRule);
Then remove the rule when you want to allow internet access again:
INetFwPolicy2 firewallPolicy = (INetFwPolicy2)Activator.CreateInstance(
Type.GetTypeFromProgID("HNetCfg.FwPolicy2"));
firewallPolicy.Rules.Remove("Block Internet");
This is a slight modification of some other code that I’ve used, so I can’t make any guarantees that it’ll work. Once again, keep in mind that you'll need Windows Vista (or later) and administrative privileges for this to work.
Link to the firewall API documentation.
This is what I am currently using (my idea, not an api):
System.Diagnostics;
void InternetConnection(string str)
{
ProcessStartInfo internet = new ProcessStartInfo()
{
FileName = "cmd.exe",
Arguments = "/C ipconfig /" + str,
WindowStyle = ProcessWindowStyle.Hidden
};
Process.Start(internet);
}
Disconnect from internet: InternetConnection("release");
Connect to internet: InternetConnection("renew");
Disconnecting will just remove the access to internet (it will show a caution icon in the wifi icon).
Connecting might take five seconds or more.
Out of topic:
In any cases you might want to check if you're connected or not (when you use the code above), I better suggest this:
System.Net.NetworkInformation;
public static bool CheckInternetConnection()
{
try
{
Ping myPing = new Ping();
String host = "google.com";
byte[] buffer = new byte[32];
int timeout = 1000;
PingOptions pingOptions = new PingOptions();
PingReply reply = myPing.Send(host, timeout, buffer, pingOptions);
return (reply.Status == IPStatus.Success);
}
catch (Exception)
{
return false;
}
}
There are actually a myriad of ways to turn off (Read: break) your internet access, but I think the simplest one would be to turn of the network interface that connects you to the internet.
Here is a link to get you started:
Identifying active network interface
Here's a sample program that does it using WMI management objects.
In the example, I'm targeting my wireless adapter by looking for network adapters that have "Wireless" in their name. You could figure out some substring that identifies the name of the adapter that you are targeting (you can get the names by doing ipconfig /all at a command line). Not passing a substring would cause this to go through all adapters, which is kinda severe. You'll need to add a reference to System.Management to your project.
using System;
using System.Management;
namespace ConsoleAdapterEnabler
{
public static class NetworkAdapterEnabler
{
public static ManagementObjectSearcher GetWMINetworkAdapters(String filterExpression = "")
{
String queryString = "SELECT * FROM Win32_NetworkAdapter";
if (filterExpression.Length > 0)
{
queryString += String.Format(" WHERE Name LIKE '%{0}%' ", filterExpression);
}
WqlObjectQuery query = new WqlObjectQuery(queryString);
ManagementObjectSearcher objectSearcher = new ManagementObjectSearcher(query);
return objectSearcher;
}
public static void EnableWMINetworkAdapters(String filterExpression = "")
{
foreach (ManagementObject adapter in GetWMINetworkAdapters(filterExpression).Get())
{
//only enable if not already enabled
if (((bool)adapter.Properties["NetEnabled"].Value) != true)
{
adapter.InvokeMethod("Enable", null);
}
}
}
public static void DisableWMINetworkAdapters(String filterExpression = "")
{
foreach (ManagementObject adapter in GetWMINetworkAdapters(filterExpression).Get())
{
//If enabled, then disable
if (((bool)adapter.Properties["NetEnabled"].Value)==true)
{
adapter.InvokeMethod("Disable", null);
}
}
}
}
class Program
{
public static int Main(string[] args)
{
NetworkAdapterEnabler.DisableWMINetworkAdapters("Wireless");
Console.WriteLine("Press any key to continue");
var key = Console.ReadKey();
NetworkAdapterEnabler.EnableWMINetworkAdapters("Wireless");
Console.WriteLine("Press any key to continue");
key = Console.ReadKey();
return 0;
}
}
}
public static void BlockingOfData()
{
INetFwPolicy2 firewallPolicy = (INetFwPolicy2)Activator.CreateInstance(Type.GetTypeFromProgID("HNetCfg.FwPolicy2"));
firewallPolicy.set_DefaultOutboundAction(NET_FW_PROFILE_TYPE2_.NET_FW_PROFILE2_DOMAIN, NET_FW_ACTION_.NET_FW_ACTION_BLOCK);
firewallPolicy.set_DefaultOutboundAction(NET_FW_PROFILE_TYPE2_.NET_FW_PROFILE2_PRIVATE, NET_FW_ACTION_.NET_FW_ACTION_BLOCK);
firewallPolicy.set_DefaultOutboundAction(NET_FW_PROFILE_TYPE2_.NET_FW_PROFILE2_PUBLIC, NET_FW_ACTION_.NET_FW_ACTION_BLOCK);
}

Categories