Skip to content

Instantly share code, notes, and snippets.

@secure-77
Created December 2, 2022 14:31
Show Gist options
  • Star 1 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save secure-77/49e8275c0d2727ebad582f1c75696eac to your computer and use it in GitHub Desktop.
Save secure-77/49e8275c0d2727ebad582f1c75696eac to your computer and use it in GitHub Desktop.
Powershell Web Shell
<#
.Synopsis
Starts powershell webserver
.Description
Starts webserver as powershell process.
Call of the root page (e.g. http://localhost:8080/) returns a powershell execution web form.
Call of /script uploads a powershell script and executes it (as a function).
Call of /log returns the webserver logs, /starttime the start time of the webserver, /time the current time.
/download downloads and /upload uploads a file. /beep generates a sound and /quit or /exit stops the webserver.
Any other call delivers the static content that fits to the path provided. If the static path is a directory,
a file index.htm, index.html, default.htm or default.html in this directory is delivered if present.
You may have to configure a firewall exception to allow access to the chosen port, e.g. with:
netsh advfirewall firewall add rule name="Powershell Webserver" dir=in action=allow protocol=TCP localport=8080
After stopping the webserver you should remove the rule, e.g.:
netsh advfirewall firewall delete rule name="Powershell Webserver"
.Parameter BINDING
Binding of the webserver
.Parameter BASEDIR
Base directory for static content (default: the script's directory)
.Inputs
None
.Outputs
None
.Example
Start-Webserver
Starts webserver with binding to http://localhost:8080/
.Example
Start-Webserver "http://+:8080/"
Starts webserver with binding to all IP addresses of the system.
Administrative rights are necessary.
.Example
schtasks.exe /Create /TN "Powershell Webserver" /TR "powershell -Command \"Start-WebServer http://+:8080/\" /SC ONSTART /RU SYSTEM /RL HIGHEST /F
Starts powershell webserver as scheduled task as user local system every time the computer starts.
You can start the webserver task manually with
schtasks.exe /Run /TN "Powershell Webserver"
Delete the webserver task with
schtasks.exe /Delete /TN "Powershell Webserver"
Scheduled tasks are running with low priority per default, so some functions might be slow.
.Notes
Version 1.1, 2017-11-23
Author: Markus Scholtes
#>
function Start-Webserver
{
Param([STRING]$BINDING = 'http://localhost:8080/', [STRING]$BASEDIR = "")
# No adminstrative permissions are required for a binding to "localhost"
# $BINDING = 'http://localhost:8080/'
# Adminstrative permissions are required for a binding to network names or addresses.
# + takes all requests to the port regardless of name or ip, * only requests that no other listener answers:
# $BINDING = 'http://+:8080/'
if ($BASEDIR -eq "")
{ # retrieve script path as base path for static content
if ($MyInvocation.MyCommand.CommandType -eq "ExternalScript")
{ $BASEDIR = Split-Path -Parent -Path $MyInvocation.MyCommand.Definition }
else # compiled with PS2EXE:
{ $BASEDIR = Split-Path -Parent -Path ([Environment]::GetCommandLineArgs()[0]) }
}
# convert to absolute path
$BASEDIR = $ExecutionContext.SessionState.Path.GetUnresolvedProviderPathFromPSPath($BASEDIR)
# MIME hash table for static content
$MIMEHASH = @{".avi"="video/x-msvideo"; ".crt"="application/x-x509-ca-cert"; ".css"="text/css"; ".der"="application/x-x509-ca-cert"; ".flv"="video/x-flv"; ".gif"="image/gif"; ".htm"="text/html"; ".html"="text/html"; ".ico"="image/x-icon"; ".jar"="application/java-archive"; ".jardiff"="application/x-java-archive-diff"; ".jpeg"="image/jpeg"; ".jpg"="image/jpeg"; ".js"="application/x-javascript"; ".mov"="video/quicktime"; ".mp3"="audio/mpeg"; ".mpeg"="video/mpeg"; ".mpg"="video/mpeg"; ".pdf"="application/pdf"; ".pem"="application/x-x509-ca-cert"; ".pl"="application/x-perl"; ".png"="image/png"; ".rss"="text/xml"; ".shtml"="text/html"; ".swf"="application/x-shockwave-flash"; ".txt"="text/plain"; ".war"="application/java-archive"; ".wmv"="video/x-ms-wmv"; ".xml"="text/xml"}
# HTML answer templates for specific calls, placeholders !RESULT, !FORMFIELD, !PROMPT are allowed
$HTMLRESPONSECONTENTS = @{
'GET /' = @"
<html><body>
!HEADERLINE
<pre>!RESULT</pre>
<form method="GET" action="/">
<b>!PROMPT&nbsp;</b><input type="text" maxlength=255 size=80 name="command" value='!FORMFIELD'>
<input type="submit" name="button" value="Enter">
</form>
</body></html>
"@
'GET /script' = @"
<html><body>
!HEADERLINE
<form method="POST" enctype="multipart/form-data" action="/script">
<p><b>Script to execute:</b><input type="file" name="filedata"></p>
<b>Parameters:</b><input type="text" maxlength=255 size=80 name="parameter">
<input type="submit" name="button" value="Execute">
</form>
</body></html>
"@
'GET /download' = @"
<html><body>
!HEADERLINE
<pre>!RESULT</pre>
<form method="POST" action="/download">
<b>Path to file:</b><input type="text" maxlength=255 size=80 name="filepath" value='!FORMFIELD'>
<input type="submit" name="button" value="Download">
</form>
</body></html>
"@
'POST /download' = @"
<html><body>
!HEADERLINE
<pre>!RESULT</pre>
<form method="POST" action="/download">
<b>Path to file:</b><input type="text" maxlength=255 size=80 name="filepath" value='!FORMFIELD'>
<input type="submit" name="button" value="Download">
</form>
</body></html>
"@
'GET /upload' = @"
<html><body>
!HEADERLINE
<form method="POST" enctype="multipart/form-data" action="/upload">
<p><b>File to upload:</b><input type="file" name="filedata"></p>
<b>Path to store on webserver:</b><input type="text" maxlength=255 size=80 name="filepath">
<input type="submit" name="button" value="Upload">
</form>
</body></html>
"@
'POST /script' = "<html><body>!HEADERLINE<pre>!RESULT</pre></body></html>"
'POST /upload' = "<html><body>!HEADERLINE<pre>!RESULT</pre></body></html>"
'GET /exit' = "<html><body>Stopped powershell webserver</body></html>"
'GET /quit' = "<html><body>Stopped powershell webserver</body></html>"
'GET /log' = "<html><body>!HEADERLINELog of powershell webserver:<br /><pre>!RESULT</pre></body></html>"
'GET /starttime' = "<html><body>!HEADERLINEPowershell webserver started at $(Get-Date -Format s)</body></html>"
'GET /time' = "<html><body>!HEADERLINECurrent time: !RESULT</body></html>"
'GET /beep' = "<html><body>!HEADERLINEBEEP...</body></html>"
}
# Set navigation header line for all web pages
$HEADERLINE = "<p><a href='/'>Command execution</a> <a href='/script'>Execute script</a> <a href='/download'>Download file</a> <a href='/upload'>Upload file</a> <a href='/log'>Web logs</a> <a href='/starttime'>Webserver start time</a> <a href='/time'>Current time</a> <a href='/beep'>Beep</a> <a href='/quit'>Stop webserver</a></p>"
# Starting the powershell webserver
"$(Get-Date -Format s) Starting powershell webserver..."
$LISTENER = New-Object System.Net.HttpListener
$LISTENER.Prefixes.Add($BINDING)
$LISTENER.Start()
$Error.Clear()
try
{
"$(Get-Date -Format s) Powershell webserver started."
$WEBLOG = "$(Get-Date -Format s) Powershell webserver started.`n"
while ($LISTENER.IsListening)
{
# analyze incoming request
$CONTEXT = $LISTENER.GetContext()
$REQUEST = $CONTEXT.Request
$RESPONSE = $CONTEXT.Response
$RESPONSEWRITTEN = $FALSE
# log to console
"$(Get-Date -Format s) $($REQUEST.RemoteEndPoint.Address.ToString()) $($REQUEST.httpMethod) $($REQUEST.Url.PathAndQuery)"
# and in log variable
$WEBLOG += "$(Get-Date -Format s) $($REQUEST.RemoteEndPoint.Address.ToString()) $($REQUEST.httpMethod) $($REQUEST.Url.PathAndQuery)`n"
# is there a fixed coding for the request?
$RECEIVED = '{0} {1}' -f $REQUEST.httpMethod, $REQUEST.Url.LocalPath
$HTMLRESPONSE = $HTMLRESPONSECONTENTS[$RECEIVED]
$RESULT = ''
# check for known commands
switch ($RECEIVED)
{
"GET /"
{ # execute command
# retrieve GET query string
$FORMFIELD = ''
$FORMFIELD = [URI]::UnescapeDataString(($REQUEST.Url.Query -replace "\+"," "))
# remove fixed form fields out of query string
$FORMFIELD = $FORMFIELD -replace "\?command=","" -replace "\?button=enter","" -replace "&command=","" -replace "&button=enter",""
# when command is given...
if (![STRING]::IsNullOrEmpty($FORMFIELD))
{
try {
# ... execute command
$RESULT = Invoke-Expression -EA SilentlyContinue $FORMFIELD 2> $NULL | Out-String
}
catch
{
# just ignore. Error handling comes afterwards since not every error throws an exception
}
if ($Error.Count -gt 0)
{ # retrieve error message on error
$RESULT += "`nError while executing '$FORMFIELD'`n`n"
$RESULT += $Error[0]
$Error.Clear()
}
}
# preset form value with command for the caller's convenience
$HTMLRESPONSE = $HTMLRESPONSE -replace '!FORMFIELD', $FORMFIELD
# insert powershell prompt to form
$PROMPT = "PS $PWD>"
$HTMLRESPONSE = $HTMLRESPONSE -replace '!PROMPT', $PROMPT
break
}
"GET /script"
{ # present upload form, nothing to do here
break
}
"POST /script"
{ # upload and execute script
# only if there is body data in the request
if ($REQUEST.HasEntityBody)
{
# set default message to error message (since we just stop processing on error)
$RESULT = "Received corrupt or incomplete form data"
# check content type
if ($REQUEST.ContentType)
{
# retrieve boundary marker for header separation
$BOUNDARY = $NULL
if ($REQUEST.ContentType -match "boundary=(.*);")
{ $BOUNDARY = "--" + $MATCHES[1] }
else
{ # marker might be at the end of the line
if ($REQUEST.ContentType -match "boundary=(.*)$")
{ $BOUNDARY = "--" + $MATCHES[1] }
}
if ($BOUNDARY)
{ # only if header separator was found
# read complete header (inkl. file data) into string
$READER = New-Object System.IO.StreamReader($REQUEST.InputStream, $REQUEST.ContentEncoding)
$DATA = $READER.ReadToEnd()
$READER.Close()
$REQUEST.InputStream.Close()
$PARAMETERS = ""
$SOURCENAME = ""
# separate headers by boundary string
$DATA -replace "$BOUNDARY--\r\n", "$BOUNDARY`r`n--" -split "$BOUNDARY\r\n" | ForEach-Object {
# omit leading empty header and end marker header
if (($_ -ne "") -and ($_ -ne "--"))
{
# only if well defined header (separation between meta data and data)
if ($_.IndexOf("`r`n`r`n") -gt 0)
{
# header data before two CRs is meta data
# first look for the file in header "filedata"
if ($_.Substring(0, $_.IndexOf("`r`n`r`n")) -match "Content-Disposition: form-data; name=(.*);")
{
$HEADERNAME = $MATCHES[1] -replace '\"'
# headername "filedata"?
if ($HEADERNAME -eq "filedata")
{ # yes, look for source filename
if ($_.Substring(0, $_.IndexOf("`r`n`r`n")) -match "filename=(.*)")
{ # source filename found
$SOURCENAME = $MATCHES[1] -replace "`r`n$" -replace "`r$" -replace '\"'
# store content of file in variable
$FILEDATA = $_.Substring($_.IndexOf("`r`n`r`n") + 4) -replace "`r`n$"
}
}
}
else
{ # look for other headers (we need "parameter")
if ($_.Substring(0, $_.IndexOf("`r`n`r`n")) -match "Content-Disposition: form-data; name=(.*)")
{ # header found
$HEADERNAME = $MATCHES[1] -replace '\"'
# headername "parameter"?
if ($HEADERNAME -eq "parameter")
{ # yes, look for paramaters
$PARAMETERS = $_.Substring($_.IndexOf("`r`n`r`n") + 4) -replace "`r`n$" -replace "`r$"
}
}
}
}
}
}
if ($SOURCENAME -ne "")
{ # execute only if a source file exists
$EXECUTE = "function Powershell-WebServer-Func {`n" + $FILEDATA + "`n}`nPowershell-WebServer-Func " + $PARAMETERS
try {
# ... execute script
$RESULT = Invoke-Expression -EA SilentlyContinue $EXECUTE 2> $NULL | Out-String
}
catch
{
# just ignore. Error handling comes afterwards since not every error throws an exception
}
if ($Error.Count -gt 0)
{ # retrieve error message on error
$RESULT += "`nError while executing script $SOURCENAME`n`n"
$RESULT += $Error[0]
$Error.Clear()
}
}
else
{
$RESULT = "No file data received"
}
}
}
}
else
{
$RESULT = "No client data received"
}
break
}
{ $_ -like "* /download" } # GET or POST method are allowed for download page
{ # download file
# is POST data in the request?
if ($REQUEST.HasEntityBody)
{ # POST request
# read complete header into string
$READER = New-Object System.IO.StreamReader($REQUEST.InputStream, $REQUEST.ContentEncoding)
$DATA = $READER.ReadToEnd()
$READER.Close()
$REQUEST.InputStream.Close()
# get headers into hash table
$HEADER = @{}
$DATA.Split('&') | ForEach-Object { $HEADER.Add([URI]::UnescapeDataString(($_.Split('=')[0] -replace "\+"," ")), [URI]::UnescapeDataString(($_.Split('=')[1] -replace "\+"," "))) }
# read header 'filepath'
$FORMFIELD = $HEADER.Item('filepath')
# remove leading and trailing double quotes since Test-Path does not like them
$FORMFIELD = $FORMFIELD -replace "^`"","" -replace "`"$",""
}
else
{ # GET request
# retrieve GET query string
$FORMFIELD = ''
$FORMFIELD = [URI]::UnescapeDataString(($REQUEST.Url.Query -replace "\+"," "))
# remove fixed form fields out of query string
$FORMFIELD = $FORMFIELD -replace "\?filepath=","" -replace "\?button=download","" -replace "&filepath=","" -replace "&button=download",""
# remove leading and trailing double quotes since Test-Path does not like them
$FORMFIELD = $FORMFIELD -replace "^`"","" -replace "`"$",""
}
# when path is given...
if (![STRING]::IsNullOrEmpty($FORMFIELD))
{ # check if file exists
if (Test-Path $FORMFIELD -PathType Leaf)
{
try {
# ... download file
$BUFFER = [System.IO.File]::ReadAllBytes($FORMFIELD)
$RESPONSE.ContentLength64 = $BUFFER.Length
$RESPONSE.SendChunked = $FALSE
$RESPONSE.ContentType = "application/octet-stream"
$FILENAME = Split-Path -Leaf $FORMFIELD
$RESPONSE.AddHeader("Content-Disposition", "attachment; filename=$FILENAME")
$RESPONSE.AddHeader("Last-Modified", [IO.File]::GetLastWriteTime($FORMFIELD).ToString('r'))
$RESPONSE.AddHeader("Server", "Powershell Webserver/1.1 on ")
$RESPONSE.OutputStream.Write($BUFFER, 0, $BUFFER.Length)
# mark response as already given
$RESPONSEWRITTEN = $TRUE
}
catch
{
# just ignore. Error handling comes afterwards since not every error throws an exception
}
if ($Error.Count -gt 0)
{ # retrieve error message on error
$RESULT += "`nError while downloading '$FORMFIELD'`n`n"
$RESULT += $Error[0]
$Error.Clear()
}
}
else
{
# ... file not found
$RESULT = "File $FORMFIELD not found"
}
}
# preset form value with file path for the caller's convenience
$HTMLRESPONSE = $HTMLRESPONSE -replace '!FORMFIELD', $FORMFIELD
break
}
"GET /upload"
{ # present upload form, nothing to do here
break
}
"POST /upload"
{ # upload file
# only if there is body data in the request
if ($REQUEST.HasEntityBody)
{
# set default message to error message (since we just stop processing on error)
$RESULT = "Received corrupt or incomplete form data"
# check content type
if ($REQUEST.ContentType)
{
# retrieve boundary marker for header separation
$BOUNDARY = $NULL
if ($REQUEST.ContentType -match "boundary=(.*);")
{ $BOUNDARY = "--" + $MATCHES[1] }
else
{ # marker might be at the end of the line
if ($REQUEST.ContentType -match "boundary=(.*)$")
{ $BOUNDARY = "--" + $MATCHES[1] }
}
if ($BOUNDARY)
{ # only if header separator was found
# read complete header (inkl. file data) into string
$READER = New-Object System.IO.StreamReader($REQUEST.InputStream, $REQUEST.ContentEncoding)
$DATA = $READER.ReadToEnd()
$READER.Close()
$REQUEST.InputStream.Close()
# variables for filenames
$FILENAME = ""
$SOURCENAME = ""
# separate headers by boundary string
$DATA -replace "$BOUNDARY--\r\n", "$BOUNDARY`r`n--" -split "$BOUNDARY\r\n" | ForEach-Object {
# omit leading empty header and end marker header
if (($_ -ne "") -and ($_ -ne "--"))
{
# only if well defined header (seperation between meta data and data)
if ($_.IndexOf("`r`n`r`n") -gt 0)
{
# header data before two CRs is meta data
# first look for the file in header "filedata"
if ($_.Substring(0, $_.IndexOf("`r`n`r`n")) -match "Content-Disposition: form-data; name=(.*);")
{
$HEADERNAME = $MATCHES[1] -replace '\"'
# headername "filedata"?
if ($HEADERNAME -eq "filedata")
{ # yes, look for source filename
if ($_.Substring(0, $_.IndexOf("`r`n`r`n")) -match "filename=(.*)")
{ # source filename found
$SOURCENAME = $MATCHES[1] -replace "`r`n$" -replace "`r$" -replace '\"'
# store content of file in variable
$FILEDATA = $_.Substring($_.IndexOf("`r`n`r`n") + 4) -replace "`r`n$"
}
}
}
else
{ # look for other headers (we need "filepath" to know where to store the file)
if ($_.Substring(0, $_.IndexOf("`r`n`r`n")) -match "Content-Disposition: form-data; name=(.*)")
{ # header found
$HEADERNAME = $MATCHES[1] -replace '\"'
# headername "filepath"?
if ($HEADERNAME -eq "filepath")
{ # yes, look for target filename
$FILENAME = $_.Substring($_.IndexOf("`r`n`r`n") + 4) -replace "`r`n$" -replace "`r$" -replace '\"'
}
}
}
}
}
}
if ($FILENAME -ne "")
{ # upload only if a targetname is given
if ($SOURCENAME -ne "")
{ # only upload if source file exists
# check or construct a valid filename to store
$TARGETNAME = ""
# if filename is a container name, add source filename to it
if (Test-Path $FILENAME -PathType Container)
{
$TARGETNAME = Join-Path $FILENAME -ChildPath $(Split-Path $SOURCENAME -Leaf)
} else {
# try name in the header
$TARGETNAME = $FILENAME
}
try {
# ... save file with the same encoding as received
[IO.File]::WriteAllText($TARGETNAME, $FILEDATA, $REQUEST.ContentEncoding)
}
catch
{
# just ignore. Error handling comes afterwards since not every error throws an exception
}
if ($Error.Count -gt 0)
{ # retrieve error message on error
$RESULT += "`nError saving '$TARGETNAME'`n`n"
$RESULT += $Error[0]
$Error.Clear()
}
else
{ # success
$RESULT = "File $SOURCENAME successfully uploaded as $TARGETNAME"
}
}
else
{
$RESULT = "No file data received"
}
}
else
{
$RESULT = "Missing target file name"
}
}
}
}
else
{
$RESULT = "No client data received"
}
break
}
"GET /log"
{ # return the webserver log (stored in log variable)
$RESULT = $WEBLOG
break
}
"GET /time"
{ # return current time
$RESULT = Get-Date -Format s
break
}
"GET /starttime"
{ # return start time of the powershell webserver (already contained in $HTMLRESPONSE, nothing to do here)
break
}
"GET /beep"
{ # Beep
[CONSOLE]::beep(800, 300) # or "`a" or [char]7
break
}
"GET /quit"
{ # stop powershell webserver, nothing to do here
break
}
"GET /exit"
{ # stop powershell webserver, nothing to do here
break
}
default
{ # unknown command, check if path to file
# create physical path based upon the base dir and url
$CHECKDIR = $BASEDIR.TrimEnd("/\") + $REQUEST.Url.LocalPath
$CHECKFILE = ""
if (Test-Path $CHECKDIR -PathType Container)
{ # physical path is a directory
$IDXLIST = "/index.htm", "/index.html", "/default.htm", "/default.html"
foreach ($IDXNAME in $IDXLIST)
{ # check if an index file is present
$CHECKFILE = $CHECKDIR.TrimEnd("/\") + $IDXNAME
if (Test-Path $CHECKFILE -PathType Leaf)
{ # index file found, path now in $CHECKFILE
break
}
$CHECKFILE = ""
}
}
else
{ # no directory, check for file
if (Test-Path $CHECKDIR -PathType Leaf)
{ # file found, path now in $CHECKFILE
$CHECKFILE = $CHECKDIR
}
}
if ($CHECKFILE -ne "")
{ # static content available
try {
# ... serve static content
$BUFFER = [System.IO.File]::ReadAllBytes($CHECKFILE)
$RESPONSE.ContentLength64 = $BUFFER.Length
$RESPONSE.SendChunked = $FALSE
$EXTENSION = [IO.Path]::GetExtension($CHECKFILE)
if ($MIMEHASH.ContainsKey($EXTENSION))
{ # known mime type for this file's extension available
$RESPONSE.ContentType = $MIMEHASH.Item($EXTENSION)
}
else
{ # no, serve as binary download
$RESPONSE.ContentType = "application/octet-stream"
$FILENAME = Split-Path -Leaf $CHECKFILE
$RESPONSE.AddHeader("Content-Disposition", "attachment; filename=$FILENAME")
}
$RESPONSE.AddHeader("Last-Modified", [IO.File]::GetLastWriteTime($CHECKFILE).ToString('r'))
$RESPONSE.AddHeader("Server", "Powershell Webserver/1.1 on ")
$RESPONSE.OutputStream.Write($BUFFER, 0, $BUFFER.Length)
# mark response as already given
$RESPONSEWRITTEN = $TRUE
}
catch
{
# just ignore. Error handling comes afterwards since not every error throws an exception
}
if ($Error.Count -gt 0)
{ # retrieve error message on error
$RESULT += "`nError while downloading '$CHECKFILE'`n`n"
$RESULT += $Error[0]
$Error.Clear()
}
}
else
{ # no file to serve found, return error
$RESPONSE.StatusCode = 404
$HTMLRESPONSE = '<html><body>Page not found</body></html>'
}
}
}
# only send response if not already done
if (!$RESPONSEWRITTEN)
{
# insert header line string into HTML template
$HTMLRESPONSE = $HTMLRESPONSE -replace '!HEADERLINE', $HEADERLINE
# insert result string into HTML template
$HTMLRESPONSE = $HTMLRESPONSE -replace '!RESULT', $RESULT
# return HTML answer to caller
$BUFFER = [Text.Encoding]::UTF8.GetBytes($HTMLRESPONSE)
$RESPONSE.ContentLength64 = $BUFFER.Length
$RESPONSE.AddHeader("Last-Modified", [DATETIME]::Now.ToString('r'))
$RESPONSE.AddHeader("Server", "Powershell Webserver/1.1 on ")
$RESPONSE.OutputStream.Write($BUFFER, 0, $BUFFER.Length)
}
# and finish answer to client
$RESPONSE.Close()
# received command to stop webserver?
if ($RECEIVED -eq 'GET /exit' -or $RECEIVED -eq 'GET /quit')
{ # then break out of while loop
"$(Get-Date -Format s) Stopping powershell webserver..."
break;
}
}
}
finally
{
# Stop powershell webserver
$LISTENER.Stop()
$LISTENER.Close()
"$(Get-Date -Format s) Powershell webserver stopped."
}
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment