from ConfigParser import ConfigParser
from StringIO import StringIO
from abc import ABCMeta, abstractmethod
+import xml.etree.ElementTree as etree
+
+class gp_log:
+ ''' Log settings overwritten by gpo apply
+ The gp_log is an xml file that stores a history of gpo changes (and the original setting value).
+
+ The log is organized like so:
+
+<gp>
+ <user name="KDC-1$">
+ <applylog>
+ <guid count="0" value="{31B2F340-016D-11D2-945F-00C04FB984F9}" />
+ </applylog>
+ <guid value="{31B2F340-016D-11D2-945F-00C04FB984F9}">
+ <gp_ext name="System Access">
+ <attribute name="minPwdAge">-864000000000</attribute>
+ <attribute name="maxPwdAge">-36288000000000</attribute>
+ <attribute name="minPwdLength">7</attribute>
+ <attribute name="pwdProperties">1</attribute>
+ </gp_ext>
+ <gp_ext name="Kerberos Policy">
+ <attribute name="ticket_lifetime">1d</attribute>
+ <attribute name="renew_lifetime" />
+ <attribute name="clockskew">300</attribute>
+ </gp_ext>
+ </guid>
+ </user>
+</gp>
+
+ Each guid value contains a list of extensions, which contain a list of attributes. The guid value
+ represents a GPO. The attributes are the values of those settings prior to the application of
+ the GPO.
+ The list of guids is enclosed within a user name, which represents the user the settings were
+ applied to. This user may be the samaccountname of the local computer, which implies that these
+ are machine policies.
+ The applylog keeps track of the order in which the GPOs were applied, so that they can be rolled
+ back in reverse, returning the machine to the state prior to policy application.
+ '''
+ def __init__(self, user, gpostore, db_log=None):
+ ''' Initialize the gp_log
+ param user - the username (or machine name) that policies are being applied to
+ param gpostore - the GPOStorage obj which references the tdb which contains gp_logs
+ param db_log - (optional) a string to initialize the gp_log
+ '''
+ self.gpostore = gpostore
+ self.username = user
+ if db_log:
+ self.gpdb = etree.fromstring(db_log)
+ else:
+ self.gpdb = etree.Element('gp')
+ self.user = self.gpdb.find('user[@name="%s"]' % user)
+ if self.user is None:
+ self.user = etree.SubElement(self.gpdb, 'user')
+ self.user.attrib['name'] = user
+
+ def set_guid(self, guid):
+ ''' Log to a different GPO guid
+ param guid - guid value of the GPO from which we're applying policy
+ '''
+ self.guid = self.user.find('guid[@value="%s"]' % guid)
+ if self.guid is None:
+ self.guid = etree.SubElement(self.user, 'guid')
+ self.guid.attrib['value'] = guid
+ apply_log = self.user.find('applylog')
+ if apply_log is None:
+ apply_log = etree.SubElement(self.user, 'applylog')
+ item = etree.SubElement(apply_log, 'guid')
+ item.attrib['count'] = '%d' % (len(apply_log)-1)
+ item.attrib['value'] = guid
+
+ def apply_log_pop(self):
+ ''' Pop a GPO guid from the applylog
+ return - last applied GPO guid
+
+ Removes the GPO guid last added to the list, which is the most recently applied GPO.
+ '''
+ apply_log = self.user.find('applylog')
+ if apply_log is not None:
+ ret = apply_log.find('guid[@count="%d"]' % (len(apply_log)-1))
+ if ret is not None:
+ apply_log.remove(ret)
+ return ret.attrib['value']
+ if len(apply_log) == 0 and apply_log in self.user:
+ self.user.remove(apply_log)
+ return None
+
+ def store(self, gp_ext_name, attribute, old_val):
+ ''' Store an attribute in the gp_log
+ param gp_ext_name - Name of the extension applying policy
+ param attribute - The attribute being modified
+ param old_val - The value of the attribute prior to policy application
+ '''
+ assert self.guid is not None, "gpo guid was not set"
+ ext = self.guid.find('gp_ext[@name="%s"]' % gp_ext_name)
+ if ext is None:
+ ext = etree.SubElement(self.guid, 'gp_ext')
+ ext.attrib['name'] = gp_ext_name
+ attr = ext.find('attribute[@name="%s"]' % attribute)
+ if attr is None:
+ attr = etree.SubElement(ext, 'attribute')
+ attr.attrib['name'] = attribute
+ attr.text = old_val
+
+ def retrieve(self, gp_ext_name, attribute):
+ ''' Retrieve a stored attribute from the gp_log
+ param gp_ext_name - Name of the extension which applied policy
+ param attribute - The attribute being retrieved
+ return - The value of the attribute prior to policy application
+ '''
+ assert self.guid is not None, "gpo guid was not set"
+ ext = self.guid.find('gp_ext[@name="%s"]' % gp_ext_name)
+ if ext is not None:
+ attr = ext.find('attribute[@name="%s"]' % attribute)
+ if attr is not None:
+ return attr.text
+ return None
+
+ def list(self, gp_extensions):
+ ''' Return a list of attributes, their previous values, and functions to set them
+ param gp_extensions - list of extension objects, for retrieving attr to func mappings
+ return - list of (attr, value, apply_func) tuples for unapplying policy
+ '''
+ assert self.guid is not None, "gpo guid was not set"
+ ret = []
+ data_maps = {}
+ for gp_ext in gp_extensions:
+ data_maps.update(gp_ext.apply_map())
+ exts = self.guid.findall('gp_ext')
+ if exts is not None:
+ for ext in exts:
+ ext_map = {val[0]: val[1] for (key, val) in data_maps[ext.attrib['name']].items()}
+ attrs = ext.findall('attribute')
+ for attr in attrs:
+ ret.append((attr.attrib['name'], attr.text, ext_map[attr.attrib['name']]))
+ return ret
-class Backlog:
- def __init__(self, sysvol_log):
- if os.path.isfile(sysvol_log):
- self.backlog = tdb.open(sysvol_log)
+ def delete(self, gp_ext_name, attribute):
+ ''' Remove an attribute from the gp_log
+ param gp_ext_name - name of extension from which to remove the attribute
+ param attribute - attribute to remove
+ '''
+ assert self.guid is not None, "gpo guid was not set"
+ ext = self.guid.find('gp_ext[@name="%s"]' % gp_ext_name)
+ if ext is not None:
+ attr = ext.find('attribute[@name="%s"]' % attribute)
+ if attr is not None:
+ ext.remove(attr)
+ if len(ext) == 0:
+ self.guid.remove(ext)
+
+ def commit(self):
+ ''' Write gp_log changes to disk '''
+ if len(self.guid) == 0 and self.guid in self.user:
+ self.user.remove(self.guid)
+ if len(self.user) == 0 and self.user in self.gpdb:
+ self.gpdb.remove(self.user)
+ self.gpostore.store(self.username, etree.tostring(self.gpdb, 'utf-8'))
+
+class GPOStorage:
+ def __init__(self, log_file):
+ if os.path.isfile(log_file):
+ self.log = tdb.open(log_file)
else:
- self.backlog = tdb.Tdb(sysvol_log, 0, tdb.DEFAULT, os.O_CREAT|os.O_RDWR)
- self.backlog.transaction_start()
+ self.log = tdb.Tdb(log_file, 0, tdb.DEFAULT, os.O_CREAT|os.O_RDWR)
- def version(self, guid):
+ def start(self):
+ self.log.transaction_start()
+
+ def get_int(self, key):
try:
- old_version = int(self.backlog.get(guid))
+ return int(self.log.get(key))
except TypeError:
- old_version = -1
- return old_version
+ return None
+
+ def get(self, key):
+ return self.log.get(key)
+
+ def get_gplog(self, user):
+ return gp_log(user, self, self.log.get(user))
+
+ def store(self, key, val):
+ self.log.store(key, val)
+
+ def cancel(self):
+ self.log.transaction_cancel()
- def store(self, guid, version):
- self.backlog.store(guid, '%i' % version)
+ def delete(self, key):
+ self.log.delete(key)
def commit(self):
- self.backlog.transaction_commit()
+ self.log.transaction_commit()
def __del__(self):
- self.backlog.close()
+ self.log.close()
class gp_ext(object):
__metaclass__ = ABCMeta
pass
@abstractmethod
- def parse(self, afile, ldb, conn, lp):
+ def apply_map(self):
pass
@abstractmethod
- def __str__(self):
+ def parse(self, afile, ldb, conn, gp_db, lp):
pass
+ @abstractmethod
+ def __str__(self):
+ pass
class inf_to():
__metaclass__ = ABCMeta
- def __init__(self, logger, ldb, lp, attribute, val):
+ def __init__(self, logger, ldb, gp_db, lp, attribute, val):
self.logger = logger
self.ldb = ldb
self.attribute = attribute
self.val = val
self.lp = lp
+ self.gp_db = gp_db
def explicit(self):
return self.val
def mapper(self):
pass
+ @abstractmethod
+ def __str__(self):
+ pass
+
class inf_to_ldb(inf_to):
'''This class takes the .inf file parameter (essentially a GPO file mapped to a GUID),
hashmaps it to the Samba parameter, which then uses an ldb object to update the
'''
def ch_minPwdAge(self, val):
- self.logger.info('KDC Minimum Password age was changed from %s to %s' % (self.ldb.get_minPwdAge(), val))
+ old_val = self.ldb.get_minPwdAge()
+ self.logger.info('KDC Minimum Password age was changed from %s to %s' % (old_val, val))
+ self.gp_db.store(str(self), self.attribute, old_val)
self.ldb.set_minPwdAge(val)
def ch_maxPwdAge(self, val):
- self.logger.info('KDC Maximum Password age was changed from %s to %s' % (self.ldb.get_maxPwdAge(), val))
+ old_val = self.ldb.get_maxPwdAge()
+ self.logger.info('KDC Maximum Password age was changed from %s to %s' % (old_val, val))
+ self.gp_db.store(str(self), self.attribute, old_val)
self.ldb.set_maxPwdAge(val)
def ch_minPwdLength(self, val):
- self.logger.info('KDC Minimum Password length was changed from %s to %s' % (self.ldb.get_minPwdLength(), val))
+ old_val = self.ldb.get_minPwdLength()
+ self.logger.info('KDC Minimum Password length was changed from %s to %s' % (old_val, val))
+ self.gp_db.store(str(self), self.attribute, old_val)
self.ldb.set_minPwdLength(val)
def ch_pwdProperties(self, val):
- self.logger.info('KDC Password Properties were changed from %s to %s' % (self.ldb.get_pwdProperties(), val))
+ old_val = self.ldb.get_pwdProperties()
+ self.logger.info('KDC Password Properties were changed from %s to %s' % (old_val, val))
+ self.gp_db.store(str(self), self.attribute, old_val)
self.ldb.set_pwdProperties(val)
def nttime2unix(self):
}
+ def __str__(self):
+ return 'System Access'
+
class gp_sec_ext(gp_ext):
'''This class does the following two things:
def listuserpol(self, rootpath):
return os.path.join(rootpath, "User/Registry.pol")
- def populate_inf(self):
+ def apply_map(self):
return {"System Access": {"MinimumPasswordAge": ("minPwdAge", inf_to_ldb),
"MaximumPasswordAge": ("maxPwdAge", inf_to_ldb),
"MinimumPasswordLength": ("minPwdLength", inf_to_ldb),
def read_inf(self, path, conn):
ret = False
- inftable = self.populate_inf()
+ inftable = self.apply_map()
policy = conn.loadfile(path.replace('/', '\\'))
current_section = None
(att, setter) = current_section.get(key)
value = value.encode('ascii', 'ignore')
ret = True
- setter(self.logger, self.ldb, self.lp, att, value).update_samba()
+ setter(self.logger, self.ldb, self.gp_db, self.lp, att, value).update_samba()
+ self.gp_db.commit()
return ret
- def parse(self, afile, ldb, conn, lp):
+ def parse(self, afile, ldb, conn, gp_db, lp):
self.ldb = ldb
+ self.gp_db = gp_db
self.lp = lp
# Fixing the bug where only some Linux Boxes capitalize MACHINE
gpos = ads.get_gpo_list(creds.get_username())
return gpos
+def apply_gp(lp, creds, test_ldb, logger, store, gp_extensions):
+ gp_db = store.get_gplog(creds.get_username())
+ dc_hostname = get_dc_hostname()
+ try:
+ conn = smb.SMB(dc_hostname, 'sysvol', lp=lp, creds=creds)
+ except:
+ logger.error('Error connecting to \'%s\' using SMB' % dc_hostname)
+ raise
+ gpos = get_gpo_list(dc_hostname, creds, lp)
+
+ for gpo_obj in gpos:
+ guid = gpo_obj.name
+ if guid == 'Local Policy':
+ continue
+ local_path = os.path.join(lp.get('realm').lower(), 'Policies', guid)
+ version = int(gpo.gpo_get_sysvol_gpt_version(os.path.join(lp.get("path", "sysvol"), local_path))[1])
+ if version != store.get_int(guid):
+ logger.info('GPO %s has changed' % guid)
+ gp_db.set_guid(guid)
+ store.start()
+ try:
+ for ext in gp_extensions:
+ ext.parse(ext.list(local_path), test_ldb, conn, gp_db, lp)
+ except:
+ logger.error('Failed to parse gpo %s' % guid)
+ store.cancel()
+ continue
+ store.store(guid, '%i' % version)
+ store.commit()
+
+def unapply_log(gp_db):
+ while True:
+ item = gp_db.apply_log_pop()
+ if item:
+ yield item
+ else:
+ break
+
+def unapply_gp(lp, creds, test_ldb, logger, store, gp_extensions):
+ gp_db = store.get_gplog(creds.get_username())
+ for gpo_guid in unapply_log(gp_db):
+ gp_db.set_guid(gpo_guid)
+ unapply_attributes = gp_db.list(gp_extensions)
+ for attr in unapply_attributes:
+ attr_obj = attr[-1](logger, test_ldb, gp_db, lp, attr[0], attr[1])
+ attr_obj.mapper()[attr[0]][0](attr[1]) # Set the old value
+ gp_db.delete(str(attr_obj), attr[0])
+ gp_db.commit()
+
if __name__ == "__main__":
parser = optparse.OptionParser('samba_gpoupdate [options]')
sambaopts = options.SambaOptions(parser)
parser.add_option_group(options.VersionOptions(parser))
credopts = options.CredentialsOptions(parser)
parser.add_option('-H', '--url', dest='url', help='URL for the samdb')
+ parser.add_option('-X', '--unapply', help='Unapply Group Policy', action='store_true')
parser.add_option_group(credopts)
# Set the options and the arguments
elif log_level >= 4:
logger.setLevel(logging.DEBUG)
- '''Return a live instance of Samba'''
- test_ldb = SamDB(url, session_info=session, credentials=creds, lp=lp)
-
- # Read the readable backLog into a hashmap
- # then open writable backLog in same location
- sysvol_log = os.path.join(lp.get('cache directory'), 'gpo.tdb')
+ cache_dir = lp.get('cache directory')
+ store = GPOStorage(os.path.join(cache_dir, 'gpo.tdb'))
- backlog = Backlog(sysvol_log)
+ gp_extensions = [gp_sec_ext(logger)]
- dc_hostname = get_dc_hostname()
- try:
- conn = smb.SMB(dc_hostname, 'sysvol', lp=lp, creds=creds)
- except:
- logger.error('Error connecting to \'%s\' using SMB' % dc_hostname)
- raise
- gpos = get_gpo_list(dc_hostname, creds, lp)
+ # Get a live instance of Samba
+ test_ldb = SamDB(url, session_info=session, credentials=creds, lp=lp)
- for gpo_obj in gpos:
- guid = gpo_obj.name
- if guid == 'Local Policy':
- continue
- gp_extensions = [gp_sec_ext(logger)]
- local_path = os.path.join(lp.get('realm').lower(), 'Policies', guid)
- version = int(gpo.gpo_get_sysvol_gpt_version(os.path.join(lp.get("path", "sysvol"), local_path))[1])
- if version != backlog.version(guid):
- logger.info('GPO %s has changed' % guid)
- try:
- for ext in gp_extensions:
- ext.parse(ext.list(local_path), test_ldb, conn, lp)
- except:
- logger.error('Failed to parse gpo %s' % guid)
- continue
- backlog.store(guid, version)
- backlog.commit()
+ if not opts.unapply:
+ apply_gp(lp, creds, test_ldb, logger, store, gp_extensions)
+ else:
+ unapply_gp(lp, creds, test_ldb, logger, store, gp_extensions)