from subprocess import Popen, PIPE import sys,argparse,re #MIT License #Copyright (c) 2020 John Page (aka hyp3rlinx) #Permission is hereby granted, free of charge, to any person obtaining a copy #of this software and associated documentation files (the "Software"), to deal #in the Software without restriction, including without limitation the rights #to use, copy, modify, merge, publish, distribute, sublicense, and/or sell #copies of the Software, and to permit persons to whom the Software is #furnished to do so, subject to the following conditions: #The above copyright notice and this permission notice shall be included in all #copies or substantial portions of the Software. #THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR #IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, #FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE #AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER #LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, #OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE #SOFTWARE. #Permission is also explicitly given for insertion in vulnerability databases and similar, #provided that due credit is given to the author John Page (aka hyp3rlinx). # # # NtFileSins v2.2 (c) # By John Page (aka hyp3rlinx) # Python v3 compatible # Enhancements: search target user dir on first pass, unless the -d flag is used, added .dat, .tmp file ext checks. # TODO: Alternate Data Streams (ADS) check e.g. abc.txt:test.txt:$DATA # Original advisory: http://hyp3rlinx.altervista.org/advisories/MICROSOFT-WINDOWS-NTFS-PRIVILEGED-FILE-ACCESS-ENUMERATION.txt # # NtFileSins is a Windows File Enumeration Intel Gathering Tool. # Standard users can prove existence of privileged user artifacts. # # Typically, the Windows commands DIR or TYPE hand out a default "Access Denied" error message, # when a file exists or doesn't exist, when restricted access is attempted by another user. # # However, accessing files directly by attempting to "open" them from cmd.exe shell, # we can determine existence by compare inconsistent Windows error messages. # # Requirements: 1) target users with >= privileges (not admin to admin). # 2) artifacts must contain a dot "." or returns false positives. # # Windows message "Access Denied" = Exists # Windows message "The system cannot find the file" = Not exists # Windows returns "no message" OR "c:\victim\artifact is not recognized as an internal or external command, # operable program or batch file" = Admin to Admin so this script is not required. # # Profile other users by compare ntfs error messages to potentially learn their activities or machines purpose. # For evil or maybe check for basic malware IOC existence on disk with user-only rights. #From a defensive perspective we can leverage this to try to detect basic IOC and malware artifacts like .tmp, .ini, .dll, .exe #or related config files on disk with user-only rights, instead of authenticating with admin rights as a quick paranoid first pass. #Example, if malware hides itself by unlinking themselves from the EPROCESS list in memory or using programs like WinRAP to hide #processess from Windows TaskMgr, we may not discover them even if using tasklist command. The EPROCESS structure and flink/blink is #how Windows TaskMgr shows all running processes. However, we may possibly detect them by testing for the correct IOC name if the #malicious code happens to reside on disk and not only in memory. Whats cool is we can be do this without the need for admin rights. # #Other Windows commands that will also let us confirm file existence by comparing error messages are start, call, copy, icalcs, and cd. #However, Windows commands rename, ren, cacls, type, dir, erase, move or del commands will issue flat out "Access is denied" messages. # #==========================================================================# # NtFileSins.py - Windows File Enumeration Intel Gathering Tool v2.2 (c) # # By John Page (aka hyp3rlinx) # # Apparition Security # #==========================================================================# BANNER=''' _ _______________ __ _____ _ / | / /_ __/ ____(_) /__ / ___/(_)___ _____ / |/ / / / / /_ / / / _ \\__ \ / / __ \/ ___/ / /| / / / / __/ / / / __/__/ / / / / (__ ) /_/ |_/ /_/ /_/ /_/_/\___/____/_/_/ /_/____/ v2.2 (c) By hyp3rlinx ApparitionSec ''' sin_cnt=0 internet_sin_cnt=0 found_set=set() zone_set=set() ARTIFACTS_SET=set() ROOTDIR = "c:/Users/" ZONE_IDENTIFIER=":Zone.Identifier:$DATA" USER_DIRS=["Contacts","Desktop","Downloads","Favorites","My Documents","Searches","Videos/Captures", "Pictures","Music","OneDrive","OneDrive/Attachments","OneDrive/Documents"] APPDATA_DIR=["AppData/Local/Temp"] EXTS = set([".contact",".url",".lnk",".search-ms",".exe",".csv",".txt",".ini",".conf",".config",".log",".pcap",".zip",".mp4",".mp3", ".bat",".tmp", ".wav",".docx",".pptx",".reg",".vcf",".avi",".mpg",".jpg",".jpeg",".png",".rtf",".pdf",".dll",".xml",".doc",".gif",".xls",".wmv",".dat"]) REPORT="NtFileSins_Log.txt" def usage(): print("NtFileSins is a privileged file access enumeration tool to search multi-account artifacts without admin rights.\n") print('-u victim -d Searches -a "MS17-020 - Google Search.url"') print('-u victim -a ""') print("-u victim -d Downloads -a -s") print('-u victim -d Contacts -a "Mike N.contact"') print("-u victim -a APT -b -n") print("-u victim -d -z Desktop/MyFiles -a <.name>") print("-u victim -d Searches -a .search-ms") print("-u victim -d . -a ") print("-u victim -d desktop -a inverted-crosses.mp3 -b") print("-u victim -d Downloads -a APT.exe -b") print("-u victim -f list_of_files.txt") print("-u victim -f list_of_files.txt -b -s") print("-u victim -f list_of_files.txt -x .txt") print("-u victim -d desktop -f list_of_files.txt -b") print("-u victim -d desktop -f list_of_files.txt -x .rar") print("-u victim -z -s -f list_of_files.txt") def parse_args(): parser.add_argument("-u", "--user", help="Privileged user target") parser.add_argument("-d", "--directory", nargs="?", help="Specific directory to search .") parser.add_argument("-a", "--artifact", help="Single artifact we want to verify exists.") parser.add_argument("-t", "--appdata", nargs="?", const="1", help="Searches the AppData/Local/Temp directory.") parser.add_argument("-f", "--artifacts_from_file", nargs="?", help="Enumerate a list of supplied artifacts from a file.") parser.add_argument("-n", "--notfound", nargs="?", const="1", help="Display unfound artifacts.") parser.add_argument("-b", "--built_in_ext", nargs="?", const="1", help="Enumerate files using NtFileSin built-in ext types.") parser.add_argument("-x", "--specific_ext", nargs="?", help="Enumerate using specific ext, e.g. <.exe> using a supplied list of artifacts, a supplied ext will override any in the supplied artifact list.") parser.add_argument("-z", "--zone_identifier", nargs="?", const="1", help="Identifies artifacts downloaded from the internet by checking for Zone.Identifier:$DATA.") #parser.add_argument("-r", "--ads_streams", nargs="?", const="1", help="Locate ADS hidden file streams (artifact name required).") parser.add_argument("-s", "--save", nargs="?", const="1", help="Saves successfully enumerated artifacts, will log to "+REPORT) parser.add_argument("-v", "--verbose", nargs="?", const="1", help="Displays the file access error messages.") parser.add_argument("-e", "--examples", nargs="?", const="1", help="Show example usage.") return parser.parse_args() def access(j): result="" try: p = Popen([j], stdout=PIPE, stderr=PIPE, shell=True) stderr,stdout = p.communicate() result = stdout.strip() res = result.decode("utf-8") except Exception as e: #print(str(e)) pass return res def artifacts_from_file(artifacts_file, bflag, specific_ext): try: f=open(artifacts_file, "r") for a in f: idx = a.rfind(".") a = a.strip() if a != "": if specific_ext: if idx==-1: a = a + specific_ext else: #replace existing ext a = a[:idx] + specific_ext if bflag: ARTIFACTS_SET.add(a) else: ARTIFACTS_SET.add(a) f.close() except Exception as e: print(str(e)) exit() def save(): try: f=open(REPORT, "w") for j in found_set: f.write(j+"\n") f.close() except Exception as e: print(str(e)) def recon_msg(s): if s == 0: return "Access is denied." else: return "\t[*] Artifact exists ==>" def echo_results(args, res, x, i): global sin_cnt if res=="": print("\t[!] No NTFS message, you must already be admin, then this script is not required.") exit() if "not recognized as an internal or external command" in res: print("\t[!] You must target users with higher privileges than yours.") exit() if res != recon_msg(0): if args.verbose: print("\t"+res) else: if args.notfound: print("\t[-] not found: " + x +"/"+ i) else: sin_cnt += 1 if args.save or args.zone_identifier: found_set.add(x+"/"+i) if args.verbose: print(recon_msg(1)+ x+"/"+i) print("\t"+res) else: print(recon_msg(1)+ x+"/"+i) def valid_artifact_name(sin,args): idx = "." in sin if re.findall(r"[/\\*?:<>|]", sin): print("\t[!] Skipping: disallowed file name character.") return False if not idx and not args.built_in_ext and not args.specific_ext: print("\t[!] Warning: '"+ sin +"' has no '.' in the artifact name, this can result in false positives.") print("\t[+] Searching for '"+ sin +"' using built-in ext list to prevent false positives.") if not args.built_in_ext: if sin[-1] == ".": print("\t[!] Skipping: "+sin+" non valid file name.") return False return True def search_missing_ext(path,args,i): res="" for x in path: for e in EXTS: res = access(ROOTDIR+"/"+x+"/"+i+e) if res=="": res = access(ROOTDIR+args.user+"/"+x+"/"+i+e) if res: echo_results(args, res, x, i+e) #Check if the found artifact was downloaded from internet def zone_identifier_check(args): global ROOTDIR, internet_sin_cnt zone_set.update(found_set) for c in found_set: c = c + ZONE_IDENTIFIER res = access(ROOTDIR+args.user+"/"+c) if res == "Access is denied.": internet_sin_cnt += 1 print("\t[$] Zone Identifier found: "+c+" this file was downloaded over the internet!.") zone_set.add(c) #@TODO: Find ADS def alternate_data_dreams(): pass def ntsins(path,args,i): res="" if i.rfind(".")==-1: search_missing_ext(path,args,i) i="" for x in path: if i != "": if args.built_in_ext=="1": for e in EXTS: #Search current targets user dir first. res = access(ROOTDIR+"/"+x+"/"+i+e) if res=="": res = access(ROOTDIR+args.user+"/"+x+"/"+i+e) if res: echo_results(args, res, x, i+e) elif args.specific_ext: idx = i.rfind(".") if idx == -1: i = i + "." else: i = i[:idx] + args.specific_ext #Search current targets user dir first. res = access(ROOTDIR+"/"+x+"/"+i) if res=="": res = access(ROOTDIR+args.user+"/"+x+"/"+i) if res: echo_results(args, res, x, i) def search(args): print("\tSearching...\n") global ROOTDIR, USER_DIRS, ARTIFACTS_SET if args.artifact: ARTIFACTS_SET = set([args.artifact]) for i in ARTIFACTS_SET: idx = i.rfind(".") + 1 if idx and args.built_in_ext: i = i[:idx -1:None] if len(i) > 0 and i != None: if valid_artifact_name(i,args): #specific user dir search if args.directory: single_dir=[args.directory] ntsins(single_dir,args,i) #search appdata dirs elif args.appdata: ntsins(APPDATA_DIR,args,i) #all default user dirs else: ntsins(USER_DIRS,args,i) def check_dir_input(_dir): if len(re.findall(r":", _dir)) != 0: print("[!] Check the directory arg, NtFileSins searches under c:/Users/target by default see Help -h.") return False return True def main(args): global USER_DIRS if len(sys.argv)==1: parser.print_help(sys.stderr) sys.exit(1) if args.examples: usage() exit() if not args.user: print("[!] No target user specified see Help -h") exit() if args.appdata and args.directory: print("[!] Multiple search directories supplied see Help -h") exit() if args.specific_ext: if "." not in args.specific_ext: print("[!] Must use full extension e.g. -x ."+args.specific_ext+", dot in filenames mandatory to prevent false positives.") exit() if args.artifact and args.artifacts_from_file: print("[!] Multiple artifacts specified, use just -f or -a see Help -h") exit() if args.built_in_ext and args.specific_ext: print("\t[!] Both specific and built-in extensions supplied, use only one.") exit() if args.specific_ext and not args.artifacts_from_file: print("\t[!] -x to be used with -f flag only see Help -h.") exit() if args.artifact: if args.artifact.rfind(".")==-1 and not args.built_in_ext: print("\t[!] Artifacts must contain a .ext or will result in false positives, use -b flag (built-in ext checks).") exit() if args.directory: if not check_dir_input(args.directory): exit() if args.artifacts_from_file: artifacts_from_file(args.artifacts_from_file, args.built_in_ext, args.specific_ext) #TODO: #if args.ads_streams: #alternate_data_dreams() if not args.artifact and not args.artifacts_from_file: print("[!] Exiting, no artifacts supplied see Help -h") exit() else: #Search targets user dir by default, instead of require -d flag to specify the dir. USER_DIRS.append(args.user) search(args) if sin_cnt >= 1 and args.zone_identifier: zone_identifier_check(args) if args.save and len(found_set) != 0 and not args.zone_identifier: save() if args.save and len(zone_set) != 0: found_set.update(zone_set) save() print("\n\tNtFileSins Detected "+str(sin_cnt)+ " out of %s" % str(len(ARTIFACTS_SET)) + " Sins.\n") if args.zone_identifier and internet_sin_cnt >= 1: print("\t"+str(internet_sin_cnt) + " of the sins were internet downloaded.\n") if not args.notfound: print("\tuse -n to display unfound enumerated files.") if not args.built_in_ext: print("\tfor extra search coverage try -b flag or targeted artifact search -a.") if __name__ == "__main__": print(BANNER) parser = argparse.ArgumentParser() main(parse_args())