diff --git a/README.md b/README.md
index 4483819..e9ed445 100644
--- a/README.md
+++ b/README.md
@@ -30,7 +30,7 @@ shame that there were no complete Windows/Active Directory enumeration tool on
 Linux.
 
 That's why I decided to rewrite some of PowerView's functionalities in Python,
-using the wonderful [impacket](https://github.com/CoreSecurity/impacket/)
+using the wonderful [impacket](https://github.com/SecureAuthCorp/impacket)
 library.
 
 *Update:* I haven't tested the last version of PowerView yet, which can run
@@ -58,8 +58,9 @@ Also, blah blah blah, don't use it for evil purposes.
 
 ## REQUIREMENTS
 
-* Python 2.7
-* impacket >= 0.9.16-dev
+* Python 3.6
+* impacket >= 0.9.22
+* ldap3 >= 2.8.1
 
 ## FUNCTIONALITIES
 
@@ -179,12 +180,13 @@ an argument, and __not__ `USELESSDOMAIN`.
   contributions.
 * Thanks to [@ThePirateWhoSmellsOfSunflowers](https://github.com/ThePirateWhoSmellsOfSunflowers)
   for his debugging, love you baby :heart:
+* Thanks to [@mpgn](https://github.com/mpgn) for his python 3 contributions.
 
 ## COPYRIGHT
 
 PywerView - A Python rewriting of PowerSploit's PowerView
 
-Yannick Méheut [yannick (at) meheut (dot) org] - Copyright © 2016
+Yannick Méheut [yannick (at) meheut (dot) org] - Copyright © 2021
 
 This program is free software: you can redistribute it and/or modify it
 under the terms of the GNU General Public License as published by the
diff --git a/debian/changelog b/debian/changelog
index 77724f0..183f1b1 100644
--- a/debian/changelog
+++ b/debian/changelog
@@ -1,3 +1,9 @@
+pywerview (0.3.1-0kali1) UNRELEASED; urgency=low
+
+  * New upstream release.
+
+ -- Kali Janitor <janitor@kali.org>  Sun, 18 Jul 2021 17:12:06 -0000
+
 pywerview (0.2.0+git20191220-0kali2) kali-dev; urgency=medium
 
   [ Sophie Brun ]
diff --git a/debian/patches/remove-require-pycrypto.patch b/debian/patches/remove-require-pycrypto.patch
index 1130950..6d3ba80 100644
--- a/debian/patches/remove-require-pycrypto.patch
+++ b/debian/patches/remove-require-pycrypto.patch
@@ -11,15 +11,15 @@ Crypto is never used in this module.
  setup.py | 1 -
  1 file changed, 1 deletion(-)
 
-diff --git a/setup.py b/setup.py
-index dfeb834..9c6a6b0 100644
---- a/setup.py
-+++ b/setup.py
+Index: pywerview/setup.py
+===================================================================
+--- pywerview.orig/setup.py
++++ pywerview/setup.py
 @@ -30,7 +30,6 @@ setup(name='pywerview',
      install_requires=[
-         'impacket>=0.9.16dev',
+         'impacket>=0.9.22',
          'pyasn1',
 -        'pycrypto',
          'pyopenssl',
-         'bs4'
-     ],
+         'bs4',
+         'ldap3>=2.8.1'
diff --git a/pywerview.py b/pywerview.py
index 64b597e..d93e5a8 100755
--- a/pywerview.py
+++ b/pywerview.py
@@ -1,5 +1,4 @@
-#!/usr/bin/env python
-# -*- coding: utf8 -*-
+#!/usr/bin/env python3
 #
 # This file is part of PywerView.
 
@@ -16,7 +15,7 @@
 # You should have received a copy of the GNU General Public License
 # along with PywerView.  If not, see <http://www.gnu.org/licenses/>.
 
-# Yannick Méheut [yannick (at) meheut (dot) org] - Copyright © 2016
+# Yannick Méheut [yannick (at) meheut (dot) org] - Copyright © 2021
 
 from pywerview.cli.main import main
 
diff --git a/pywerview/cli/helpers.py b/pywerview/cli/helpers.py
index b74f520..911942a 100644
--- a/pywerview/cli/helpers.py
+++ b/pywerview/cli/helpers.py
@@ -1,5 +1,4 @@
 #!/usr/bin/env python
-# -*- coding: utf8 -*-
 #
 # This file is part of PywerView.
 
@@ -16,7 +15,7 @@
 # You should have received a copy of the GNU General Public License
 # along with PywerView.  If not, see <http://www.gnu.org/licenses/>.
 
-# Yannick Méheut [yannick (at) meheut (dot) org] - Copyright © 2016
+# Yannick Méheut [yannick (at) meheut (dot) org] - Copyright © 2021
 
 from pywerview.functions.net import NetRequester
 from pywerview.functions.gpo import GPORequester
@@ -37,13 +36,15 @@ def get_adobject(domain_controller, domain, user, password=str(),
 def get_netuser(domain_controller, domain, user, password=str(), lmhash=str(),
                 nthash=str(), queried_username=str(), queried_domain=str(), ads_path=str(),
                 admin_count=False, spn=False, unconstrained=False, allow_delegation=False,
-                preauth_notreq=False, custom_filter=str()):
+                preauth_notreq=False, custom_filter=str(),
+                attributes=[]):
     requester = NetRequester(domain_controller, domain, user, password,
-                                 lmhash, nthash)
+                             lmhash, nthash)
     return requester.get_netuser(queried_username=queried_username,
                                     queried_domain=queried_domain, ads_path=ads_path, admin_count=admin_count,
                                     spn=spn, unconstrained=unconstrained, allow_delegation=allow_delegation,
-                                    preauth_notreq=preauth_notreq, custom_filter=custom_filter)
+                                    preauth_notreq=preauth_notreq, custom_filter=custom_filter,
+                                    attributes=attributes)
 
 def get_netgroup(domain_controller, domain, user, password=str(),
                 lmhash=str(), nthash=str(), queried_groupname='*', queried_sid=str(),
@@ -60,14 +61,14 @@ def get_netcomputer(domain_controller, domain, user, password=str(),
                     lmhash=str(), nthash=str(), queried_computername='*', queried_spn=str(),
                     queried_os=str(), queried_sp=str(), queried_domain=str(), ads_path=str(),
                     printers=False, unconstrained=False, ping=False, full_data=False,
-                    custom_filter=str()):
+                    custom_filter=str(), attributes=[]):
     requester = NetRequester(domain_controller, domain, user, password,
                                  lmhash, nthash)
     return requester.get_netcomputer(queried_computername=queried_computername,
                                         queried_spn=queried_spn, queried_os=queried_os, queried_sp=queried_sp,
                                         queried_domain=queried_domain, ads_path=ads_path, printers=printers,
                                         unconstrained=unconstrained, ping=ping, full_data=full_data,
-                                        custom_filter=custom_filter)
+                                        custom_filter=custom_filter, attributes=attributes)
 
 def get_netdomaincontroller(domain_controller, domain, user, password=str(),
                                  lmhash=str(), nthash=str(), queried_domain=str()):
@@ -100,20 +101,22 @@ def get_netou(domain_controller, domain, user, password=str(), lmhash=str(),
 
 def get_netsite(domain_controller, domain, user, password=str(), lmhash=str(),
                 nthash=str(), queried_domain=str(), queried_sitename=str(),
-                queried_guid=str(), ads_path=str(), full_data=False):
+                queried_guid=str(), ads_path=str(), ads_prefix='CN=Sites,CN=Configuration',
+                full_data=False):
     requester = NetRequester(domain_controller, domain, user, password,
                                  lmhash, nthash)
     return requester.get_netsite(queried_domain=queried_domain,
                                     queried_sitename=queried_sitename, queried_guid=queried_guid,
-                                    ads_path=ads_path, full_data=full_data)
+                                    ads_path=ads_path, ads_prefix=ads_prefix, full_data=full_data)
 
 def get_netsubnet(domain_controller, domain, user, password=str(),
                   lmhash=str(), nthash=str(), queried_domain=str(), queried_sitename=str(),
-                  ads_path=str(), full_data=False):
+                  ads_path=str(), ads_prefix='CN=Sites,CN=Configuration', full_data=False):
     requester = NetRequester(domain_controller, domain, user, password,
                                  lmhash, nthash)
     return requester.get_netsubnet(queried_domain=queried_domain,
-                                       queried_sitename=queried_sitename, ads_path=ads_path, full_data=full_data)
+                                       queried_sitename=queried_sitename, ads_path=ads_path, ads_prefix=ads_prefix,
+                                       full_data=full_data)
 
 def get_netdomaintrust(domain_controller, domain, user, password=str(),
                   lmhash=str(), nthash=str(), queried_domain=str()):
diff --git a/pywerview/cli/main.py b/pywerview/cli/main.py
index 0849b45..0abe72a 100644
--- a/pywerview/cli/main.py
+++ b/pywerview/cli/main.py
@@ -1,5 +1,4 @@
-#!/usr/bin/env python
-# -*- coding: utf8 -*-
+#!/usr/bin/env python3
 #
 # This file is part of PywerView.
 
@@ -16,7 +15,7 @@
 # You should have received a copy of the GNU General Public License
 # along with PywerView.  If not, see <http://www.gnu.org/licenses/>.
 
-# Yannick Méheut [yannick (at) meheut (dot) org] - Copyright © 2016
+# Yannick Méheut [yannick (at) meheut (dot) org] - Copyright © 2021
 
 import argparse
 from pywerview.cli.helpers import *
@@ -25,7 +24,10 @@ from pywerview.functions.hunting import *
 def main():
     # Main parser
     parser = argparse.ArgumentParser(description='Rewriting of some PowerView\'s functionalities in Python')
-    subparsers = parser.add_subparsers(title='Subcommands', description='Available subcommands')
+    subparsers = parser.add_subparsers(title='Subcommands', description='Available subcommands', dest='submodule')
+    
+    # hack for python < 3.9 : https://stackoverflow.com/questions/23349349/argparse-with-required-subparser
+    subparsers.required = True
 
     # TODO: support keberos authentication
     # Credentials parser
@@ -112,6 +114,8 @@ def main():
             help='Query only users with not-null Service Principal Names')
     get_netuser_parser.add_argument('--custom-filter', dest='custom_filter',
             default=str(), help='Custom filter')
+    get_netuser_parser.add_argument('--attributes', nargs='+', dest='attributes',
+            default=[], help='Object attributes to return')
     get_netuser_parser.set_defaults(func=get_netuser)
 
     # Parser for the get-netgroup command
@@ -156,6 +160,8 @@ def main():
             help='Ping computers (will only return up computers)')
     get_netcomputer_parser.add_argument('--full-data', action='store_true',
             help='If set, returns full information on the groups, otherwise, just the dnsHostName')
+    get_netcomputer_parser.add_argument('--attributes', nargs='+', dest='attributes',
+            default=[], help='Object attributes to return')
     get_netcomputer_parser.set_defaults(func=get_netcomputer)
 
     # Parser for the get-netdomaincontroller command
@@ -435,8 +441,6 @@ def main():
     invoke_eventhunter_parser.set_defaults(func=invoke_eventhunter)
 
     args = parser.parse_args()
-    if hasattr(args,'queried_groupname'):
-        args.queried_groupname = args.queried_groupname.encode('utf-8').decode('latin1')
     if args.hashes:
         try:
             args.lmhash, args.nthash = args.hashes.split(':')
@@ -453,7 +457,7 @@ def main():
 
     parsed_args = dict()
     for k, v in vars(args).items():
-        if k not in ('func', 'hashes'):
+        if k not in ('func', 'hashes', 'submodule'):
             parsed_args[k] = v
 
     #try:
@@ -465,10 +469,8 @@ def main():
     if results is not None:
         try:
             for x in results:
-                x = str(x).encode('latin1').decode('utf-8')
-                print(x)
-                if '\n' in x:
-                    print('')
+                    print(x)
+        # for example, invoke_checklocaladminaccess returns a bool 
         except TypeError:
             print(results)
 
diff --git a/pywerview/functions/gpo.py b/pywerview/functions/gpo.py
index f9baba0..c805332 100644
--- a/pywerview/functions/gpo.py
+++ b/pywerview/functions/gpo.py
@@ -1,5 +1,3 @@
-# -*- coding: utf8 -*-
-
 # This file is part of PywerView.
 
 # PywerView is free software: you can redistribute it and/or modify
@@ -15,7 +13,7 @@
 # You should have received a copy of the GNU General Public License
 # along with PywerView.  If not, see <http://www.gnu.org/licenses/>.
 
-# Yannick Méheut [yannick (at) meheut (dot) org] - Copyright © 2016
+# Yannick Méheut [yannick (at) meheut (dot) org] - Copyright © 2021
 
 import codecs
 from bs4 import BeautifulSoup
@@ -73,7 +71,11 @@ class GPORequester(LDAPRequester):
                 property_name, property_values = [x.strip() for x in l.split('=')]
                 if ',' in property_values:
                     property_values = property_values.split(',')
-                setattr(getattr(gpttmpl_final, section_name), property_name, property_values)
+                try:
+                    setattr(getattr(gpttmpl_final, section_name), property_name, property_values)
+                except UnicodeEncodeError:
+                    property_name = property_name.encode('utf-8')
+                    setattr(getattr(gpttmpl_final, section_name), property_name, property_values)
 
         return gpttmpl_final
 
diff --git a/pywerview/functions/hunting.py b/pywerview/functions/hunting.py
index 3fa1dc9..d1393c4 100644
--- a/pywerview/functions/hunting.py
+++ b/pywerview/functions/hunting.py
@@ -1,5 +1,3 @@
-# -*- coding: utf8 -*-
-
 # This file is part of PywerView.
 
 # PywerView is free software: you can redistribute it and/or modify
@@ -15,7 +13,7 @@
 # You should have received a copy of the GNU General Public License
 # along with PywerView.  If not, see <http://www.gnu.org/licenses/>.
 
-# Yannick Méheut [yannick (at) meheut (dot) org] - Copyright © 2016
+# Yannick Méheut [yannick (at) meheut (dot) org] - Copyright © 2021
 
 import random
 import multiprocessing
diff --git a/pywerview/functions/misc.py b/pywerview/functions/misc.py
index 1d960f2..8b772ec 100644
--- a/pywerview/functions/misc.py
+++ b/pywerview/functions/misc.py
@@ -1,5 +1,3 @@
-# -*- coding: utf8 -*-
-
 # This file is part of PywerView.
 
 # PywerView is free software: you can redistribute it and/or modify
@@ -15,7 +13,7 @@
 # You should have received a copy of the GNU General Public License
 # along with PywerView.  If not, see <http://www.gnu.org/licenses/>.
 
-# Yannick Méheut [yannick (at) meheut (dot) org] - Copyright © 2016
+# Yannick Méheut [yannick (at) meheut (dot) org] - Copyright © 2021
 
 from impacket.dcerpc.v5.rpcrt import DCERPCException
 from impacket.dcerpc.v5 import scmr, drsuapi
diff --git a/pywerview/functions/net.py b/pywerview/functions/net.py
index b7d82e1..8c996fc 100644
--- a/pywerview/functions/net.py
+++ b/pywerview/functions/net.py
@@ -1,5 +1,3 @@
-# -*- coding: utf8 -*-
-
 # This file is part of PywerView.
 
 # PywerView is free software: you can redistribute it and/or modify
@@ -15,7 +13,7 @@
 # You should have received a copy of the GNU General Public License
 # along with PywerView.  If not, see <http://www.gnu.org/licenses/>.
 
-# Yannick Méheut [yannick (at) meheut (dot) org] - Copyright © 2016
+# Yannick Méheut [yannick (at) meheut (dot) org] - Copyright © 2021
 
 import socket
 import datetime
@@ -25,6 +23,7 @@ from impacket.dcerpc.v5.samr import DCERPCSessionError
 from impacket.dcerpc.v5.rpcrt import DCERPCException
 from impacket.dcerpc.v5.dcom.wmi import WBEM_FLAG_FORWARD_ONLY
 from bs4 import BeautifulSoup
+from ldap3.utils.conv import escape_filter_chars
 
 from pywerview.requester import LDAPRPCRequester
 import pywerview.objects.adobjects as adobj
@@ -50,7 +49,7 @@ class NetRequester(LDAPRPCRequester):
                     ads_path=str(), admin_count=False, spn=False,
                     unconstrained=False, allow_delegation=False,
                     preauth_notreq=False,
-                    custom_filter=str()):
+                    custom_filter=str(), attributes=[]):
 
         if unconstrained:
             custom_filter += '(userAccountControl:1.2.840.113556.1.4.803:=524288)'
@@ -71,7 +70,7 @@ class NetRequester(LDAPRPCRequester):
 
         user_search_filter = '(&{})'.format(user_search_filter)
 
-        return self._ldap_search(user_search_filter, adobj.User)
+        return self._ldap_search(user_search_filter, adobj.User, attributes=attributes)
 
     @LDAPRPCRequester._ldap_connection_init
     def get_netgroup(self, queried_groupname='*', queried_sid=str(),
@@ -79,12 +78,18 @@ class NetRequester(LDAPRPCRequester):
                      ads_path=str(), admin_count=False, full_data=False,
                      custom_filter=str()):
 
+        # RFC 4515, section 3
+        # However if we escape *, we can no longer use wildcard within `--groupname`
+        # Maybe we can raise a warning here ? 
+        if not '*' in queried_groupname:
+            queried_groupname = escape_filter_chars(queried_groupname)
+
         if queried_username:
             results = list()
             sam_account_name_to_resolve = [queried_username]
             first_run = True
             while sam_account_name_to_resolve:
-                sam_account_name = sam_account_name_to_resolve.pop(0)
+                sam_account_name = escape_filter_chars(sam_account_name_to_resolve.pop(0))
                 if first_run:
                     first_run = False
                     if admin_count:
@@ -136,14 +141,13 @@ class NetRequester(LDAPRPCRequester):
                 attributes=['samaccountname']
 
             group_search_filter = '(&{})'.format(group_search_filter)
-
             return self._ldap_search(group_search_filter, adobj.Group, attributes=attributes)
 
     @LDAPRPCRequester._ldap_connection_init
     def get_netcomputer(self, queried_computername='*', queried_spn=str(),
                         queried_os=str(), queried_sp=str(), queried_domain=str(),
                         ads_path=str(), printers=False, unconstrained=False,
-                        ping=False, full_data=False, custom_filter=str()):
+                        ping=False, full_data=False, custom_filter=str(), attributes=[]):
 
         if unconstrained:
             custom_filter += '(userAccountControl:1.2.840.113556.1.4.803:=524288)'
@@ -161,7 +165,8 @@ class NetRequester(LDAPRPCRequester):
         if full_data:
             attributes=list()
         else:
-            attributes=['dnsHostName']
+            if not attributes:
+                attributes=['dnsHostName']
 
         computer_search_filter = '(&{})'.format(computer_search_filter)
 
@@ -183,13 +188,16 @@ class NetRequester(LDAPRPCRequester):
             if len(split_path) >= 3:
                 return split_path[2]
 
+        file_server_attributes = ['homedirectory', 'scriptpath', 'profilepath']
         results = set()
         if target_users:
             users = list()
             for target_user in target_users:
-                users += self.get_netuser(target_user, queried_domain)
+                users += self.get_netuser(target_user, queried_domain,
+                        attributes=file_server_attributes)
         else:
-            users = self.get_netuser(queried_domain=queried_domain)
+            users = self.get_netuser(queried_domain=queried_domain,
+                    attributes=file_server_attributes)
 
         for user in users:
             for full_path in (user.homedirectory, user.scriptpath, user.profilepath):
@@ -283,7 +291,8 @@ class NetRequester(LDAPRPCRequester):
 
     @LDAPRPCRequester._ldap_connection_init
     def get_netsite(self, queried_domain=str(), queried_sitename=str(),
-                    queried_guid=str(), ads_path=str(), full_data=False):
+                    queried_guid=str(), ads_path=str(), ads_prefix=str(),
+                    full_data=False):
 
         site_search_filter = '(objectCategory=site)'
 
@@ -304,7 +313,7 @@ class NetRequester(LDAPRPCRequester):
 
     @LDAPRPCRequester._ldap_connection_init
     def get_netsubnet(self, queried_domain=str(), queried_sitename=str(),
-                      ads_path=str(), full_data=False):
+                      ads_path=str(), ads_prefix=str(), full_data=False):
 
         subnet_search_filter = '(objectCategory=subnet)'
 
@@ -330,10 +339,13 @@ class NetRequester(LDAPRPCRequester):
 
         def _get_members(_groupname=str(), _sid=str()):
             try:
+                # `--groupname` option is supplied
                 if _groupname:
                     groups = self.get_netgroup(queried_groupname=_groupname,
                                                queried_domain=queried_domain,
                                                full_data=True)
+
+                # `--groupname` option is missing, falling back to the "Domain Admins"
                 else:
                     if _sid:
                         queried_sid = _sid
@@ -362,6 +374,8 @@ class NetRequester(LDAPRPCRequester):
                     # TODO: range cycling
                     try:
                         for member in group.member:
+                            # RFC 4515, section 3
+                            member = escape_filter_chars(member, encoding='utf-8')
                             dn_filter = '(distinguishedname={}){}'.format(member, custom_filter)
                             members += self.get_netuser(custom_filter=dn_filter, queried_domain=queried_domain)
                             members += self.get_netgroup(custom_filter=dn_filter, queried_domain=queried_domain, full_data=True)
@@ -382,17 +396,17 @@ class NetRequester(LDAPRPCRequester):
                         member_domain = str()
                     is_group = (member.samaccounttype != '805306368')
 
-                    attributes = list()
+                    attributes = dict()
                     if queried_domain:
-                        attributes.append({'type': 'groupdomain', 'vals': [queried_domain]})
+                        attributes['groupdomain'] = queried_domain
                     else:
-                        attributes.append({'type': 'groupdomain', 'vals': [self._domain]})
-                    attributes.append({'type': 'groupname', 'vals': [group.name]})
-                    attributes.append({'type': 'membername', 'vals': [member.samaccountname]})
-                    attributes.append({'type': 'memberdomain', 'vals': [member_domain]})
-                    attributes.append({'type': 'isgroup', 'vals': [is_group]})
-                    attributes.append({'type': 'memberdn', 'vals': [member_dn]})
-                    attributes.append({'type': 'membersid', 'vals': [member.objectsid]})
+                        attributes['groupdomain'] = self._domain
+                    attributes['groupname'] = group.name
+                    attributes['membername'] = member.samaccountname
+                    attributes['memberdomain'] = member_domain
+                    attributes['isgroup'] = is_group
+                    attributes['memberdn'] = member_dn
+                    attributes['membersid'] = member.objectsid
 
                     final_member.add_attributes(attributes)
 
diff --git a/pywerview/objects/adobjects.py b/pywerview/objects/adobjects.py
index 385117c..908fee8 100644
--- a/pywerview/objects/adobjects.py
+++ b/pywerview/objects/adobjects.py
@@ -1,5 +1,3 @@
-# -*- coding: utf8 -*-
-
 # This file is part of PywerView.
 
 # PywerView is free software: you can redistribute it and/or modify
@@ -15,9 +13,9 @@
 # You should have received a copy of the GNU General Public License
 # along with PywerView.  If not, see <http://www.gnu.org/licenses/>.
 
-# Yannick Méheut [yannick (at) meheut (dot) org] - Copyright © 2016
+# Yannick Méheut [yannick (at) meheut (dot) org] - Copyright © 2021
 
-from datetime import datetime
+from datetime import datetime, timedelta
 import inspect
 import struct
 import pyasn1
@@ -52,45 +50,61 @@ class ADObject:
 
     def add_attributes(self, attributes):
         for attr in attributes:
-            t = str(attr['type']).lower()
+            #print(attr)
+            #print(attributes[attr], attr)
+            t = str(attr).lower()
             if t in ('logonhours', 'msds-generationid'):
-                value = str(attr['vals'][0])
-                value = [ord(x) for x in value]
+                value = bytes(attributes[attr][0])
+                value = [x for x in value]
             elif t in ('trustattributes', 'trustdirection', 'trusttype'):
-                value = int(attr['vals'][0])
+                value = int(attributes[attr][0])
             elif t in ('objectsid', 'ms-ds-creatorsid'):
-                value = codecs.encode(bytes(attr['vals'][0]),'hex')
-                init_value = bytes(attr['vals'][0])
-                value = 'S-1-5'
+                value = codecs.encode(bytes(attributes[attr][0]),'hex')
+                init_value = bytes(attributes[attr][0])
+                value = 'S-{0}-{1}'.format(init_value[0], init_value[1])
                 for i in range(8, len(init_value), 4):
                     value += '-{}'.format(str(struct.unpack('<I', init_value[i:i+4])[0]))
             elif t == 'objectguid':
-                init_value = bytes(attr['vals'][0])
+                init_value = bytes(attributes[attr][0])
                 value = str()
                 value += '{}-'.format(hex(struct.unpack('<I', init_value[0:4])[0])[2:].zfill(8))
                 value += '{}-'.format(hex(struct.unpack('<H', init_value[4:6])[0])[2:].zfill(4))
                 value += '{}-'.format(hex(struct.unpack('<H', init_value[6:8])[0])[2:].zfill(4))
-                value += '{}-'.format(codecs.encode(init_value,'hex')[16:20])
+                value += '{}-'.format((codecs.encode(init_value,'hex')[16:20]).decode('utf-8'))
                 value += init_value.hex()[20:]
             elif t in ('dscorepropagationdata', 'whenchanged', 'whencreated'):
                 value = list()
-                for val in attr['vals']:
-                    value.append(str(datetime.strptime(str(val), '%Y%m%d%H%M%S.0Z')))
-            elif t in ('pwdlastset', 'badpasswordtime', 'lastlogon', 'lastlogoff'):
-                timestamp = (int(str(attr['vals'][0])) - 116444736000000000)/10000000
-                value = datetime.fromtimestamp(timestamp)
+                for val in attributes[attr]:
+                    value.append(str(datetime.strptime(str(val.decode('utf-8')), '%Y%m%d%H%M%S.0Z')))
+            elif t in ('accountexpires', 'pwdlastset', 'badpasswordtime', 'lastlogontimestamp', 'lastlogon', 'lastlogoff'):
+                try:
+                    filetimestamp = int(attributes[attr][0].decode('utf-8'))
+                    if filetimestamp != 9223372036854775807:
+                        timestamp = (filetimestamp - 116444736000000000)/10000000
+                        value = datetime.fromtimestamp(0) + timedelta(seconds=timestamp)
+                    else:
+                        value = 'never'
+                except IndexError:
+                    value = 'empty'
             elif t == 'isgroup':
-                value = attr['vals'][0]
+                value = attributes[attr]
             elif t == 'objectclass':
-                value = [str(x) for x in attr['vals']]
+                value = [x.decode('utf-8') for x in attributes[attr]]
                 setattr(self, 'isgroup', ('group' in value))
-            elif len(attr['vals']) > 1:
-                value = [str(x) for x in attr['vals']]
+            elif len(attributes[attr]) > 1:
+                try:
+                    value = [x.decode('utf-8') for x in attributes[attr]]
+                except (UnicodeDecodeError):
+                    value = [x for x in attributes[attr]]
+                except (AttributeError):
+                    value = attributes[attr]
             else:
                 try:
-                    value = str(attr['vals'][0])
-                except (IndexError, pyasn1.error.PyAsn1Error):
+                    value = attributes[attr][0].decode('utf-8')
+                except (IndexError):
                     value = str()
+                except (UnicodeDecodeError):
+                    value = attributes[attr][0]
 
             setattr(self, t, value)
 
@@ -105,7 +119,7 @@ class ADObject:
         for member in members:
             if not member[0].startswith('_'):
                 if member[0] == 'msmqdigests':
-                    member_value = (',\n' + ' ' * (max_length + 2)).join(x.encode('utf-8').hex() for x in member[1])
+                    member_value = (',\n' + ' ' * (max_length + 2)).join(x.hex() for x in member[1])
                 elif member[0] == 'useraccountcontrol':
                     member_value = list()
                     for uac_flag, uac_label in ADObject.__uac_flags.items():
@@ -117,7 +131,7 @@ class ADObject:
                     elif member[0] in ('usercertificate',
                                        'protocom-sso-entries', 'protocom-sso-security-prefs',):
                         member_value = (',\n' + ' ' * (max_length + 2)).join(
-                                '{}...'.format(x.encode('utf-8').hex()[:100]) for x in member[1])
+                                '{}...'.format(x.hex()[:100]) for x in member[1])
                     else:
                         member_value = (',\n' + ' ' * (max_length + 2)).join(str(x) for x in member[1])
                 elif member[0] in('msmqsigncertificates', 'userparameters',
@@ -126,7 +140,11 @@ class ADObject:
                                   'msrtcsip-userroutinggroupid', 'msexchumpinchecksum',
                                   'protocom-sso-auth-data', 'protocom-sso-entries-checksum',
                                   'protocom-sso-security-prefs-checksum', ):
-                    member_value = '{}...'.format(member[1].encode('utf-8').hex()[:100])
+                    # Attribut exists but it is empty
+                    try:
+                        member_value = '{}...'.format(member[1].hex()[:100])
+                    except AttributeError:
+                        member_value = ''
                 else:
                     member_value = member[1]
                 s += '{}: {}{}\n'.format(member[0], ' ' * (max_length - len(member[0])), member_value)
@@ -140,7 +158,9 @@ class ADObject:
 class User(ADObject):
     def __init__(self, attributes):
         ADObject.__init__(self, attributes)
-        for attr in ('homedirectory', 'scriptpath', 'profilepath'):
+        for attr in filter(lambda _: _ in attributes, ('homedirectory',
+                                                       'scriptpath',
+                                                       'profilepath')):
             if not hasattr(self, attr):
                 setattr(self, attr, str())
 
@@ -193,15 +213,26 @@ class Trust(ADObject):
         ad_obj = ADObject(attributes)
         self.targetname = ad_obj.name
 
+        self.trustdirection = Trust.__trust_direction.get(ad_obj.trustdirection, 'unknown')
+        self.trusttype = Trust.__trust_type.get(ad_obj.trusttype, 'unknown')
+        self.whencreated = ad_obj.whencreated
+        self.whenchanged = ad_obj.whenchanged
+
         self.trustattributes = list()
         for attrib_flag, attrib_label in Trust.__trust_attrib.items():
             if ad_obj.trustattributes & attrib_flag:
                 self.trustattributes.append(attrib_label)
 
-        self.trustdirection = Trust.__trust_direction.get(ad_obj.trustdirection, 'unknown')
-        self.trusttype = Trust.__trust_type.get(ad_obj.trusttype, 'unknown')
-        self.whencreated = ad_obj.whencreated
-        self.whenchanged = ad_obj.whenchanged
+        # If the filter SIDs attribute is not manually set, we check if we're
+        # not in a use case where SIDs are implicitly filtered
+        # Based on https://github.com/vletoux/pingcastle/blob/master/Healthcheck/TrustAnalyzer.cs
+        if 'filter_sids' not in self.trustattributes:
+            if not (self.trustdirection == 'disabled' or \
+                    self.trustdirection == 'inbound' or \
+                    'within_forest' in self.trustattributes or \
+                    'pim_trust' in self.trustattributes):
+                if 'forest_transitive' in self.trustattributes and 'treat_as_external' not in self.trustattributes:
+                    self.trustattributes.append('filter_sids')
 
 class GPO(ADObject):
     pass
diff --git a/pywerview/objects/rpcobjects.py b/pywerview/objects/rpcobjects.py
index 482fa27..67bbb43 100644
--- a/pywerview/objects/rpcobjects.py
+++ b/pywerview/objects/rpcobjects.py
@@ -1,5 +1,3 @@
-# -*- coding: utf8 -*-
-
 # This file is part of PywerView.
 
 # PywerView is free software: you can redistribute it and/or modify
@@ -15,7 +13,7 @@
 # You should have received a copy of the GNU General Public License
 # along with PywerView.  If not, see <http://www.gnu.org/licenses/>.
 
-# Yannick Méheut [yannick (at) meheut (dot) org] - Copyright © 2016
+# Yannick Méheut [yannick (at) meheut (dot) org] - Copyright © 2021
 
 from __future__ import unicode_literals
 
@@ -38,12 +36,7 @@ class RPCObject:
                        'wkui1_oth_domains', 'wkui1_username',
                        'sesi10_cname', 'sesi10_username'):
                 value = value.rstrip('\x00')
-            if isinstance(value, str):
-                try:
-                    value = value
-                except UnicodeDecodeError:
-                    pass
-
+            
             setattr(self, key.lower(), value)
 
     def __str__(self):
@@ -58,7 +51,7 @@ class RPCObject:
             if not member[0].startswith('_'):
                 s += '{}: {}{}\n'.format(member[0], ' ' * (max_length - len(member[0])), member[1])
 
-        s = s[:-1].encode('utf-8')
+        s = s[:-1]
         return s
 
     def __repr__(self):
diff --git a/pywerview/requester.py b/pywerview/requester.py
index 802dd86..715f299 100644
--- a/pywerview/requester.py
+++ b/pywerview/requester.py
@@ -1,5 +1,3 @@
-# -*- coding: utf8 -*-
-
 # This file is part of PywerView.
 
 # PywerView is free software: you can redistribute it and/or modify
@@ -15,11 +13,12 @@
 # You should have received a copy of the GNU General Public License
 # along with PywerView.  If not, see <http://www.gnu.org/licenses/>.
 
-# Yannick Méheut [yannick (at) meheut (dot) org] - Copyright © 2016
+# Yannick Méheut [yannick (at) meheut (dot) org] - Copyright © 2021
 
 import socket
 import ntpath
-from impacket.ldap import ldap, ldapasn1
+import ldap3
+
 from impacket.smbconnection import SMBConnection
 from impacket.dcerpc.v5.rpcrt import RPC_C_AUTHN_LEVEL_PKT_PRIVACY
 from impacket.dcerpc.v5 import transport, wkst, srvs, samr, scmr, drsuapi, epm
@@ -41,6 +40,7 @@ class LDAPRequester():
         self._ads_path = None
         self._ads_prefix = None
         self._ldap_connection = None
+        self._base_dn = None
 
     def _get_netfqdn(self):
         try:
@@ -79,48 +79,72 @@ class LDAPRequester():
         else:
             base_dn += ','.join('dc={}'.format(x) for x in self._queried_domain.split('.'))
 
-        try:
-            ldap_connection = ldap.LDAPConnection('ldap://{}'.format(self._domain_controller),
-                                                  base_dn, self._domain_controller)
-            ldap_connection.login(self._user, self._password, self._domain,
-                                  self._lmhash, self._nthash)
-        except ldap.LDAPSessionError as e:
-            if str(e).find('strongerAuthRequired') >= 0:
-                # We need to try SSL
-                ldap_connection = ldap.LDAPConnection('ldaps://{}'.format(self._domain_controller),
-                                                      base_dn, self._domain_controller)
-                ldap_connection.login(self._user, self._password, self._domain,
-                                      self._lmhash, self._nthash)
-            else:
-                raise e
-        except socket.error as e:
+        # base_dn is no longer used within `_create_ldap_connection()`, but I don't want to break
+        # the function call. So we store it in an attriute and use it in `_ldap_search()`
+        self._base_dn = base_dn
+        
+        # Format the username and the domain
+        # ldap3 seems not compatible with USER@DOMAIN format
+        user = '{}\\{}'.format(self._domain, self._user)
+        
+        # Choose between password or pth  
+        # TODO: Test the SSL fallbck, is cert verification disabled ?  
+        if self._lmhash and self._nthash:
+            lm_nt_hash  = '{}:{}'.format(self._lmhash, self._nthash)
+            
+            ldap_server = ldap3.Server('ldap://{}'.format(self._domain_controller))
+            
+            try:
+                ldap_connection = ldap3.Connection(ldap_server, user, lm_nt_hash, 
+                                                   authentication = ldap3.NTLM)
+            except ldap3.core.exceptions.LDAPStrongerAuthRequiredResult as e:
+                # We need to try SSL (pth version)
+                if str(e).find('strongerAuthRequired') >= 0:
+                    ldap_server = ldap3.Server('ldaps://{}'.format(self._domain_controller))
+                    ldap_connection = ldap3.Connection(ldap_server, user, lm_nt_hash, 
+                                                       authentication = ldap3.NTLM)
+                
+        else:
+            ldap_server = ldap3.Server('ldap://{}'.format(self._domain_controller))
+            try:
+                ldap_connection = ldap3.Connection(ldap_server, user, self._password,
+                                                   authentication = ldap3.NTLM)
+            except ldap3.core.exceptions.LDAPStrongerAuthRequiredResult as e:
+                # We nedd to try SSL (password version)
+                if str(e).find('strongerAuthRequired') >= 0:
+                    ldap_server = ldap3.Server('ldaps://{}'.format(self._domain_controller))
+                    ldap_connection = ldap3.Connection(ldap_server, user, self._password,
+                                                       authentication = ldap3.NTLM)        
+
+        # TODO: exit on error ?
+        if not ldap_connection.bind():
+            print('error in bind', ldap_connection.result())
             return
 
         self._ldap_connection = ldap_connection
 
     def _ldap_search(self, search_filter, class_result, attributes=list()):
         results = list()
-        paged_search_control = ldapasn1.SimplePagedResultsControl(criticality=True,
-                                                                   size=1000)
-        try:
-            search_results = self._ldap_connection.search(searchFilter=search_filter,
-                                                          searchControls=[paged_search_control],
-                                                          attributes=attributes)
-        except ldap.LDAPSearchError as e:
-            # If we got a "size exceeded" error, we get the partial results
-            if e.error == 4:
-                search_results = e.answers
-            else:
-                raise e
-        # TODO: Filter parenthesis in LDAP filter
-        except ldap.LDAPFilterSyntaxError as e:
-            return list()
-
+       
+        # if no attribute name specified, we return all attributes 
+        if not attributes:
+            attributes =  ldap3.ALL_ATTRIBUTES 
+
+        try: 
+            # Microsoft Active Directory set an hard limit of 1000 entries returned by any search
+            search_results=self._ldap_connection.extend.standard.paged_search(search_base=self._base_dn,
+                    search_filter=search_filter, attributes=attributes,
+                    paged_size=1000, generator=True)
+        # TODO: for debug only
+        except Exception as e:
+            import sys
+            print('Except: ', sys.exc_info()[0])
+
+        # Skip searchResRef
         for result in search_results:
-            if not isinstance(result, ldapasn1.SearchResultEntry):
+            if result['type'] is not 'searchResEntry':
                 continue
-
-            results.append(class_result(result['attributes']))
+            results.append(class_result(result['raw_attributes']))
 
         return results
 
@@ -136,7 +160,7 @@ class LDAPRequester():
                (ads_path != instance._ads_path) or \
                (ads_prefix != instance._ads_prefix):
                 if instance._ldap_connection:
-                    instance._ldap_connection.close()
+                    instance._ldap_connection.unbind()
                 instance._create_ldap_connection(queried_domain=queried_domain,
                                                  ads_path=ads_path, ads_prefix=ads_prefix)
             return f(*args, **kwargs)
@@ -148,7 +172,7 @@ class LDAPRequester():
 
     def __exit__(self, type, value, traceback):
         try:
-            self._ldap_connection.close()
+            self._ldap_connection.unbind()
         except AttributeError:
             pass
         self._ldap_connection = None
diff --git a/pywerview/worker/hunting.py b/pywerview/worker/hunting.py
index f808e99..e052204 100644
--- a/pywerview/worker/hunting.py
+++ b/pywerview/worker/hunting.py
@@ -1,5 +1,3 @@
-# -*- coding: utf8 -*-
-
 # This file is part of PywerView.
 
 # PywerView is free software: you can redistribute it and/or modify
@@ -15,7 +13,7 @@
 # You should have received a copy of the GNU General Public License
 # along with PywerView.  If not, see <http://www.gnu.org/licenses/>.
 
-# Yannick Méheut [yannick (at) meheut (dot) org] - Copyright © 2016
+# Yannick Méheut [yannick (at) meheut (dot) org] - Copyright © 2021
 
 from multiprocessing import Process, Pipe
 
diff --git a/requirements.txt b/requirements.txt
index a587981..fbf5fbe 100644
--- a/requirements.txt
+++ b/requirements.txt
@@ -1,2 +1,2 @@
-impacket>=0.9.16
+impacket>=0.9.22
 bs4
diff --git a/setup.py b/setup.py
index dfeb834..641b78e 100644
--- a/setup.py
+++ b/setup.py
@@ -1,4 +1,4 @@
-# -*- coding: utf8 -*-
+#!/usr/bin/env python3
 
 from setuptools import setup, find_packages
 
@@ -9,14 +9,14 @@ except(IOError, ImportError):
     long_description = open('README.md').read()
 
 setup(name='pywerview',
-    version='0.2.0',
+    version='0.3.1',
     description='A Python port of PowerSploit\'s PowerView',
     long_description=long_description,
-    dependency_links = ['https://github.com/CoreSecurity/impacket/tarball/master#egg=impacket-0.9.16dev'],
+    dependency_links = ['https://github.com/SecureAuthCorp/impacket/tarball/master#egg=impacket-0.9.22'],
     classifiers=[
         'Environment :: Console',
         'License :: OSI Approved :: GNU General Public License v3 or later (GPLv3+)',
-        'Programming Language :: Python :: 2.7',
+        'Programming Language :: Python :: 3.6',
         'Topic :: Security',
     ],
     keywords='python powersploit pentesting recon active directory windows',
@@ -28,11 +28,12 @@ setup(name='pywerview',
         "pywerview", "pywerview.*"
     ]),
     install_requires=[
-        'impacket>=0.9.16dev',
+        'impacket>=0.9.22',
         'pyasn1',
         'pycrypto',
         'pyopenssl',
-        'bs4'
+        'bs4',
+        'ldap3>=2.8.1'
     ],
     entry_points = {
         'console_scripts': ['pywerview=pywerview.cli.main:main'],