Privileger: Now you’re in control of privileges in Windows

In Windows, privileges play a key role: only the admin has the authority to grant special rights to users so that they can perform their tasks. This article discusses a software tool called Privileger: it enables you to search the system for accounts with certain privileges and change privileges assigned to a given account.

One day, during yet another pentesting audit, I realized that I have to code everything from scratch every time. To avoid this boring repetitive task, I needed a ready-made tool for manipulations with privileges. So, I wrote Privileger and, much to my surprise, it became quite popular among hackers pentesters.

Tool released!
Tool released!

This article explains the operation principle of this tool and examines its five modes that enable you to:

  • add privileges to a local account by calling just one function. In the past, this could be done only via GPO and, if my memory serves me right, it also required a host reboot, which wasn’t very convenient;
  • start a process with a specific privilege added to its token;
  • remove the privilege from an account. Again, in the past, GPO was used for this purpose; while now you just call a function;
  • detect an account with the required privilege on a given PC; and 
  • examine privileges assigned to certain account on a given PC.

Adding privileges to an account

Similar to any other project written in the C language, everything starts with the main() function that receives all the required parameters. To ensure that Cyrillic (and other) characters are displayed correctly, setlocale() is called. Then the program displays a nice ASCII banner and starts validating the input data.

int wmain(int argc, wchar_t* argv[]) {
setlocale(LC_ALL, "");
ShowAwesomeBanner();
DWORD dwRC = 0, dwV = 0;
if (argc != 4) {
ShowHelp();
return 0;
}
switch (*argv[1]) {
case '1':
if (ValidateAccInfo(argv[2], argv[3]) == 0) {
dwRC = InitMode1(argv[2], argv[3]);
}
break;
case '2':
if (ValidatePathInfo(argv[2], argv[3]) == 0) {
dwRC = InitMode2(argv[2], argv[3]);
}
break;
case '3':
if (ValidateAccInfo(argv[2], argv[3]) == 0) {
dwRC = InitMode3(argv[2], argv[3]);
}
break;
case '4':
if (ValidatePriv(argv[3])) {
dwRC = InitMode4(argv[2], argv[3]);
}
else {
std::wcout << L"[-] ValidatePriv() Failed" << std::endl;
}
break;
case '5':
std::wcout << L"[!] I'm not able to validate username and PC name. Make sure you enter the correct data." << std::endl;
Sleep(500);
std::wcout << L"[!] Starting" << std::endl;
if (InitMode5(argv[2], argv[3]) != 0) {
std::wcout << L"[-] InitMode 5 Error" << std::endl;
}
break;
default:
std::wcout << L"[-] No such mode" << std::endl;
return 0;
}
return dwRC;
}

If you’re going to use the first operating mode (i.e. add privileges to an account), you have to provide the following input data:

  • 1 (operating mode);
  • username to assign a privilege to; and 
  • programmatic name of the privilege (i.e. which privilege to be added).

The programmatic name of a privilege is its name per se. There is also a so-called display name that contains its description. For instance, the programmatic name of a privilege is SeDebugPrivilege; while its display name is Debug programs.

So, you execute the following command:

.\Privilegerx64.exe 1 Michael SeDebugPrivilege
Privilege successfully added
Privilege successfully added

To prevent typos, the program validates the username and the privilege name. Validation is implemented using the ValidateAccInfo() function, which receives the username and the programmatic name of a privilege.

DWORD ValidateAccInfo(wchar_t* cAccName, wchar_t* cPrivName) {
// validating username
DWORD sid_size = 0;
PSID UserSid;
LPTSTR wSidStr = NULL;
DWORD domain_size = 0;
SID_NAME_USE sid_use;
DWORD wow = LookupAccountName(NULL, cAccName, NULL, &sid_size, NULL, &domain_size, &sid_use);
DWORD dw = GetLastError();
if ((wow == 0) && ( (dw == 122) || (dw == 0))) {
std::wcout << L"[+] User " << cAccName << L" found" << std::endl;
// validating Privilege name
if (!ValidatePriv(cPrivName)) {
std::wcout << L"[-] ValidateAccInfo() failed" << std::endl;
return 1;
}
else {
std::wcout << L"[+] ValidateAccInfo() success" << std::endl;
return 0;
}
}
else {
std::wcout << L"[-] Username may be incorrect. LookupAccountName() Err: " << dw << std::endl;
return 1;
}
return 1;
}
Incorrect privilege name
Incorrect privilege name
Incorrect username
Incorrect username

Username is validated using the LookupAccountName() function. Originally, it’s used to retrieve user’s SID (security identifier) by its name, but nothing prevents you from using it to validate the username: if the computer doesn’t find a user with such a name, then the function call will result in an error.

Programmatic name of a privilege is also validated by a separate function.

BOOL ValidatePriv(wchar_t* cPrivName) {
LUID luid;
if (!LookupPrivilegeValue(NULL, cPrivName, &luid)) {
std::wcout << L"[-] Privilege " << cPrivName << L" may be incorrect" << std::endl;
return FALSE;
}
else {
std::wcout << L"[+] Privilege " << cPrivName << L" Found \n[+] Validation Success" << std::endl;
return TRUE;
}
}

A similar algorithm is used: the program calls LookupPrivilegeValue(): if such a privilege exists, everything is fine; if not, an error occurs.

After making sure that the input data are correct, the tool calls InitMode1() and passes to it the username and privilege name. Inside this function, you get a handle to the LSA of the current computer (since you are dealing with local account privileges) by calling the GetPolicy() function.

DWORD InitMode1(wchar_t* cAccName, wchar_t* cPrivName) {
std::wcout << L"[+] Initializing mode 1 \n [+] Target Account: " << cAccName << "\n [+] Privilege: " << cPrivName << std::endl;
LSA_HANDLE hPolicy;
if (GetPolicy(&hPolicy) != 0) {
std::wcout << L" [-] GetPolicy() Error: " << std::endl;
return 1;
}
AddUserPrivilege(hPolicy, cAccName, cPrivName, TRUE);
return 0;
}
DWORD GetPolicy(PLSA_HANDLE LsaHandle){
wchar_t cCompName[MAX_COMPUTERNAME_LENGTH + 1] = { 0 };
DWORD size = sizeof(cCompName);
if (GetComputerNameW(cCompName, &size)) {
std::wcout << L" [+] ComputerName: " << cCompName << std::endl;
}
else {
std::wcout << L" [-] GetComputerNameW Error: " << GetLastError() << std::endl;
}
LSA_OBJECT_ATTRIBUTES lsaOA = { 0 };
LSA_UNICODE_STRING lsastrComputer = { 0 };
lsaOA.Length = sizeof(lsaOA);
lsastrComputer.Length = (USHORT)(lstrlen(cCompName) * sizeof(WCHAR));
lsastrComputer.MaximumLength = lsastrComputer.Length + sizeof(WCHAR);
lsastrComputer.Buffer = (PWSTR)&cCompName;
NTSTATUS ntStatus = LsaOpenPolicy(&lsastrComputer, &lsaOA, POLICY_ALL_ACCESS, LsaHandle);
ULONG lErr = LsaNtStatusToWinError(ntStatus);
if (lErr != ERROR_SUCCESS) {
std::wcout << L" [-] LsaOpenPolicy() Error: " << lErr << std::endl;
return 1;
}
else {
std::wcout << L" [+] LsaOpenPolicy() Success" << std::endl;
return 0;
}
return 1;
}

The program uses the LsaOpenPolicy function to get a handle to the LSA policy. A distinct feature of LSA is that it uses its own error codes that cannot be retrieved using GetLastError(). You have to receive the NTSTATUS value from each function that works with LSA and then pass this value to LsaNtStatusToWinError() to convert it into a human-understandable error code.

Based on the error code, you can find out what’s wrong (see the official documentation).

If the system returns to you the correct handle and you don’t see the message ERROR_ACCESS_DENIED, then you can use the AddUserPrivilege() function to assign a privilege to the user.

DWORD AddUserPrivilege(LSA_HANDLE hPolicy, LPWSTR wUsername, LPWSTR wPrivName, BOOL bEnable) {
PSID UserSid;
DWORD sid_size = 0;
LPTSTR wSidStr = NULL;
DWORD domain_size = 0;
SID_NAME_USE sid_use;
if (!LookupAccountName(NULL, wUsername, NULL, &sid_size, NULL, &domain_size, &sid_use)) {
UserSid = (PSID)VirtualAlloc(NULL, sid_size, MEM_RESERVE | MEM_COMMIT, PAGE_READWRITE);
LPTSTR domain = NULL;
domain = (LPTSTR)VirtualAlloc(NULL, domain_size * sizeof(WCHAR), MEM_RESERVE | MEM_COMMIT, PAGE_READWRITE);
LookupAccountName(NULL, wUsername, UserSid, &sid_size, domain, &domain_size, &sid_use);
VirtualFree(domain, 0, MEM_RELEASE);
ConvertSidToStringSid(UserSid, &wSidStr);
std::wcout << L" [+] User SID: " << wSidStr << std::endl;
LSA_UNICODE_STRING lsastrPrivs[1] = { 0 };
lsastrPrivs[0].Buffer = (PWSTR)wPrivName;
lsastrPrivs[0].Length = lstrlen(lsastrPrivs[0].Buffer) * sizeof(WCHAR);
lsastrPrivs[0].MaximumLength = lsastrPrivs[0].Length + sizeof(WCHAR);
if (bEnable) {
NTSTATUS ntStatus = LsaAddAccountRights(hPolicy, UserSid, lsastrPrivs, 1);
ULONG lErr = LsaNtStatusToWinError(ntStatus);
if (lErr == ERROR_SUCCESS) {
std::wcout << L" [+] Adding " << wPrivName << L" Success" << std::endl;
std::wcout << L" [+] Enumerating Current Privs" << std::endl;
PrintTrusteePrivs(hPolicy, UserSid);
VirtualFree(UserSid, 0, MEM_RELEASE);
return 0;
}
else {
wprintf(L" [-] Error LsaAddAccountRights() %d", lErr);
return 1;
}
} else {
ULONG lErr = LsaRemoveAccountRights(hPolicy, UserSid, FALSE, lsastrPrivs, 1);
if (lErr == ERROR_SUCCESS) {
std::wcout << L" [-] Removing " << wPrivName << L" Success" << std::endl;
std::wcout << L" [+] Enumerating Current Privs" << std::endl;
PrintTrusteePrivs(hPolicy, UserSid);
VirtualFree(UserSid, 0, MEM_RELEASE);
return 0;
}
else {
wprintf(L" [-] Error LsaRemoveAccountRights() %d", lErr);
return 1;
}
}
}
else {
std::wcout << L" [-] LookupAccountName() Error: " << GetLastError() << std::endl;
return 1;
}
return 1;
}

It takes several stages to add a privilege to a user:

  1. User’s SID is identified on the basis of its username;
  2. LSA_UNICODE_STRING containing the name of the privilege that has to be added is generated;
  3. LsaAddAccountRights() is called; and 
  4. Program checks whether the privilege has been successfully added.

To identify user’s SID on the basis of its name, the above-mentioned function LookupAccountName() is used. The trick is as follows: if you pass NULL as the buffer where SID should be stored, then the function will return the buffer size required to store this SID. This is how you find out its size. Then you allocate memory for this buffer and get the SID.

if (!LookupAccountName(NULL, wUsername, NULL, &sid_size, NULL, &domain_size, &sid_use)) {
UserSid = (PSID)VirtualAlloc(NULL, sid_size, MEM_RESERVE | MEM_COMMIT, PAGE_READWRITE);
LPTSTR domain = NULL;
domain = (LPTSTR)VirtualAlloc(NULL, domain_size * sizeof(WCHAR), MEM_RESERVE | MEM_COMMIT, PAGE_READWRITE);
LookupAccountName(NULL, wUsername, UserSid, &sid_size, domain, &domain_size, &sid_use);
VirtualFree(domain, 0, MEM_RELEASE);
ConvertSidToStringSid(UserSid, &wSidStr);
std::wcout << L" [+] User SID: " << wSidStr << std::endl;

ConvertSidToStringSid() can be called to convert the SID structure into a string containing this SID,.

The privilege name must be written to the LSA_UNICODE_STRING structure that looks as follows:

typedef struct _LSA_UNICODE_STRING {
USHORT Length;
USHORT MaximumLength;
PWSTR Buffer;
} LSA_UNICODE_STRING, *PLSA_UNICODE_STRING;

The structure can be filled either manually (as I did in this project) or using a separate function (as in my tool that injects Kerberos tickets).

LSA_UNICODE_STRING lsastrPrivs[1] = { 0 };
lsastrPrivs[0].Buffer = (PWSTR)wPrivName;
lsastrPrivs[0].Length = lstrlen(lsastrPrivs[0].Buffer) * sizeof(WCHAR);
lsastrPrivs[0].MaximumLength = lsastrPrivs[0].Length + sizeof(WCHAR);

Finally, the LsaAddAccountRights() function is called.

if (bEnable) {
NTSTATUS ntStatus = LsaAddAccountRights(hPolicy, UserSid, lsastrPrivs, 1);
ULONG lErr = LsaNtStatusToWinError(ntStatus);
if (lErr == ERROR_SUCCESS) {
std::wcout << L" [+] Adding " << wPrivName << L" Success" << std::endl;
std::wcout << L" [+] Enumerating Current Privs" << std::endl;
PrintTrusteePrivs(hPolicy, UserSid);
VirtualFree(UserSid, 0, MEM_RELEASE);
return 0;
}
else {
wprintf(L" [-] Error LsaAddAccountRights() %d", lErr);
return 1;
}

The if (bEnable) condition makes it possible to use the AddUserPrivilege() function not only to add, but also to remove privileges from an account (to be addressed a bit later). The function that adds a privilege doesn’t require anything special: LSA handle, user SID, and name of the privilege to be assigned to this user. If the privilege has been successfully added, then the PrintTrusteePrivs()function is called: it displays all privileges assigned to a given account.

DWORD PrintTrusteePrivs(LSA_HANDLE hPolicy, PSID psid) {
BOOL fSuccess = FALSE;
WCHAR szTempPrivBuf[256];
WCHAR szPrivDispBuf[1024];
PLSA_UNICODE_STRING plsastrPrivs = NULL;
__try {
ULONG lCount = 0;
NTSTATUS ntStatus = LsaEnumerateAccountRights(hPolicy, psid, &plsastrPrivs, &lCount);
ULONG lErr = LsaNtStatusToWinError(ntStatus);
if (lErr != ERROR_SUCCESS) {
plsastrPrivs = NULL;
__leave;
}
ULONG lDispLen = 0;
ULONG lDispLang = 0;
for (ULONG lIndex = 0; lIndex < lCount; lIndex++) {
lstrcpyn(szTempPrivBuf, plsastrPrivs[lIndex].Buffer, plsastrPrivs[lIndex].Length);
szTempPrivBuf[plsastrPrivs[lIndex].Length] = 0;
wprintf(L"\tProgrammatic name: %s\n", szTempPrivBuf);
lDispLen = 1024;
if (LookupPrivilegeDisplayNameW(NULL, szTempPrivBuf, szPrivDispBuf, &lDispLen, &lDispLang))
wprintf(L"\tDisplay Name: %ws\n\n", szPrivDispBuf);
}
fSuccess = TRUE;
}
__finally {
if (plsastrPrivs) LsaFreeMemory(plsastrPrivs);
}
return (fSuccess);
}

Using the function LsaEnumerateAccountRights(), you find out the number of user privileges; while their names will be stored in the buffer PLSA_UNICODE_STRING. Then you parse them in a loop and concurrently find out their display names using LookupPrivilegeDisplayNameW().

This is how privileges are added to an account.

Starting a process with a privilege

A distinct feature of the second operating mode is that you have to verify not only the privilege name, but also the path (otherwise, funny exceptions might occur in the midst of a pentesting study). I implemented the validation of this information in the ValidatePathInfo() function.

DWORD ValidatePathInfo(wchar_t* Path, wchar_t* cPrivName) {
BOOL bPathEx = PathFileExistsW(Path);
if (bPathEx) {
std::wcout << L"[+] " << Path << L" Found" << std::endl;
if (!ValidatePriv(cPrivName)) {
std::wcout << L"[-] ValidatePathInfo() success" << std::endl;
return 1;
}
else {
std::wcout << L"[+] ValidatePathInfo() success" << std::endl;
return 0;
}
return 0;
}
else {
std::wcout << L"[-] " << Path << L" not found" << std::endl;
return 1;
}
return 1;
}

The simplest way to validate the path (i.e. make sure that the file path specified by the user actually exists) is to use the PathFileExists function. It returns TRUE if the path exists and FALSE if it doesn’t.

After validating the input data, the program proceeds to InitMode2():

DWORD InitMode2(wchar_t* cPath, wchar_t* cPrivName) {
std::wcout << L"[+] Initializing mode 2 \n [+] Path to exe: " << cPath << "\n [+] Privilege: " << cPrivName << std::endl;
ImpersonateSelf(SecurityImpersonation);
HANDLE hToken = NULL;
OpenThreadToken(GetCurrentThread(), TOKEN_ALL_ACCESS, FALSE, &hToken);
DWORD dw = ::GetLastError();
if (dw != 0) {
std::wcout << L"[!] Error OpenThreadToken(): " << dw << std::endl;
return 1;
}
if (EnableTokenPrivilege(hToken, cPrivName, TRUE) == 0) {
std::wcout << L" [+] EnableTokenPrivilege() success" << std::endl;
STARTUPINFO startupInfo;
ZeroMemory(&startupInfo, sizeof(STARTUPINFO));
PROCESS_INFORMATION processInformation;
ZeroMemory(&processInformation, sizeof(PROCESS_INFORMATION));
startupInfo.cb = sizeof(STARTUPINFO);
CreateProcessWithTokenW(hToken, LOGON_WITH_PROFILE, NULL, cPath, 0, NULL, NULL, &startupInfo, &processInformation);
DWORD dw = ::GetLastError();
if (dw != 0) {
std::wcout << L"[!] Error CreateProcessWithTokenW(): " << dw << std::endl;
return 1;
}
else {
std::wcout << L"[+] CreateProcessWithTokenW() success" << std::endl;
return 0;
}
}
else {
std::wcout << L" [-] EnableTokenPrivilege() failed" << std::endl;
return 1;
}
return 0;
}

This process can be divided into several stages:

  1. Current process token is assigned to current thread token;
  2. A handle to this token is received;
  3. A privilege is added to the token; and 
  4. The process with the modified token starts.

The first two steps are pretty simple. A process token can be assigned to the token of the current thread using ImpersonateSelf(); then you get a handle to it using OpenThreadToken(). Again, a separate function is used to add privileges. Such modularity makes it possible to take a function from one project and simply copy it to another one.

DWORD EnableTokenPrivilege(HANDLE hToken, LPTSTR szPriv, BOOL bEnabled) {
TOKEN_PRIVILEGES tp;
LUID luid;
if (!LookupPrivilegeValue(NULL, szPriv, &luid)) {
std::wcout << L"[-] LookupPrivilegeValue() Error: " << GetLastError() << std::endl;
return 1;
}
tp.PrivilegeCount = 1;
tp.Privileges[0].Luid = luid;
tp.Privileges[0].Attributes = bEnabled ? SE_PRIVILEGE_ENABLED : 0;
if (!AdjustTokenPrivileges(hToken, FALSE, &tp, sizeof(TOKEN_PRIVILEGES), NULL, NULL)) {
std::wcout << L"[-] AdjustTokenPrivileges() Error: " << GetLastError() << std::endl;
return 1;
}
return 0;
}

First, the program creates a special structure and passes the privilege LUID to it. LUID is an interpretation of a specific privilege in the system; based on it, Windows understands what privilege it’s dealing with. You can use LookupPrivilegeValue() to get LUID. To add the privilege to a token, you pass the newly-created structure to the AdjustTokenPrivileges function. Note that you can pass several privileges to it at once. For instance, you can simultaneously add SeDebugPrivilege and SeImpersonatePrivilege, but in this particular case, it’s not required.

After that, the thread of execution returns to the InitMode2() function; using it, the program calls the CreateProcessWithToken() function that creates a process with the token containing the newly-added privileges.

.\Privilegerx64.exe 2 C:\Windows\System32\cmd.exe SeDebugPrivilege
Process with required privileges started successfully
Process with required privileges started successfully

Removing privileges from an account

Sometimes you have to perform the opposite operation: remove a certain privilege from an account. The beginning of this procedure is similar to the first operating mode: the program validates the username and the privilege. Next, you get a handle to the LSA policy of the current PC and call AddUserPrivilege(). Remember this function?

DWORD AddUserPrivilege(LSA_HANDLE hPolicy, LPWSTR wUsername, LPWSTR wPrivName, BOOL bEnable)

If you pass FALSE as the last parameter, the following condition will be triggered.

else {
ULONG lErr = LsaRemoveAccountRights(hPolicy, UserSid, FALSE, lsastrPrivs, 1);
if (lErr == ERROR_SUCCESS) {
std::wcout << L" [-] Removing " << wPrivName << L" Success" << std::endl;
std::wcout << L" [+] Enumerating Current Privs" << std::endl;
PrintTrusteePrivs(hPolicy, UserSid);
VirtualFree(UserSid, 0, MEM_RELEASE);
return 0;
}
else {
wprintf(L" [-] Error LsaRemoveAccountRights() %d", lErr);
return 1;
}
}

As a result, the privilege will be removed from the specified account by calling the LsaRemoveAccountRights function. Then PrintTrusteePrivs() is called to display the updated list of user’s privileges.

.\Privilegerx64.exe 3 Michael SeDebugPrivilege

Searching for objects with certain privileges

In this mode, you have to pass to Privileger the name of privilege you are looking for and the name of computer to be searched for it. For instance, if you want to detect all accounts having the SeDebugPrivilege privilege on the WINPC workstation, use the following command:

.\Privilegerx64.exe 4 WINPC SeDebugPrivilege

In this mode, I decided not to validate the computer name: if you specify an invalid name, the LsaOpenPolicy() function will return error code 1722.

The entire operation has been implemented in an elegant function listed below:

DWORD InitMode4(wchar_t* cCompName, wchar_t* cPrivName) {
LSA_OBJECT_ATTRIBUTES lsaOA = { 0 };
LSA_UNICODE_STRING lsastrComputer = { 0 };
LSA_HANDLE hPolicy = NULL;
lsaOA.Length = sizeof(lsaOA);
lsastrComputer.Length = (USHORT)(lstrlen(cCompName) * sizeof(WCHAR));
lsastrComputer.MaximumLength = lsastrComputer.Length + sizeof(WCHAR);
lsastrComputer.Buffer = (PWSTR)cCompName;
NTSTATUS ntStatus = LsaOpenPolicy(&lsastrComputer, &lsaOA, POLICY_VIEW_LOCAL_INFORMATION | POLICY_LOOKUP_NAMES, &hPolicy);
ULONG lErr = LsaNtStatusToWinError(ntStatus);
if (lErr != ERROR_SUCCESS) {
if (lErr == 1722) {
std::wcout << L"[-] LsaOpenPolicy() failed: " << lErr << " | Is computer alive?" << std::endl;
return 1;
}
std::wcout << L"[-] LsaOpenPolicy() failed: " << lErr << std::endl;
return 1;
}
LSA_UNICODE_STRING privilege = { 0 };
LSA_ENUMERATION_INFORMATION* array = { 0 };
ULONG count;
WCHAR accountName[256];
WCHAR domainName[256];
SID_NAME_USE snu;
DWORD domainLength = sizeof(domainName) / sizeof(WCHAR);
DWORD accountLength = sizeof(accountName) / sizeof(WCHAR);
BOOL fSuccess = FALSE;
LPTSTR StringSid = NULL;
privilege.Length = (USHORT)(lstrlen(cPrivName) * sizeof(WCHAR));
privilege.MaximumLength = privilege.Length + sizeof(WCHAR);
privilege.Buffer = cPrivName;
__try {
NTSTATUS ntstatus = LsaEnumerateAccountsWithUserRight(hPolicy, &privilege, (void**)&array, &count);
ULONG lErr = LsaNtStatusToWinError(ntstatus);
if (lErr != ERROR_SUCCESS) {
array = NULL;
if (lErr == 259) {
std::wcout << L" [-] No objects" << std::endl;
}
else {
std::wcout << L" [-] LsaEnumerateAccountsWithUserRight() failed: " << lErr << std::endl;
}
__leave;
}
std::wcout << L"[+] Objects with privileges: " << std::endl;
for (ULONG i = 0; i < count; i++) {
ConvertSidToStringSid(array[i].Sid, &StringSid);
LookupAccountSid(NULL, array[i].Sid, accountName, &accountLength, domainName, &domainLength, &snu);
switch (snu) {
case SidTypeUser:
printf(" [!] User: ");
wprintf(L"%s\\%s %s \n", domainName, accountName, StringSid);
break;
case SidTypeGroup:
case SidTypeWellKnownGroup:
printf(" [!] Group: ");
wprintf(L"%s\\%s %s\n", domainName, accountName, StringSid);
break;
case SidTypeAlias:
printf(" [!] Alias SID (may be local group): \t");
wprintf(L"%s\\%s %s\n", domainName, accountName, StringSid);
break;
default:
printf(" [!] Idk what is it: ");
wprintf(L"%s\\%s %s\n", domainName, accountName, StringSid);
break;
}
}
fSuccess = TRUE;
}
__finally {
LsaFreeMemory(array);
}
return 0;
}

First, the function tries to get an LSA policy handle on the computer whose name was specified. For this purpose, the standard LsaOpenPolicy()is used. Then the function creates special structures to be passed to LsaEnumerateAccountsWithUserRight(). The output of this function represents an array of objects that have the required privilege. Once again, LSA_UNICODE_STRING is used to store and pass privileges. The function receives an array of objects and starts parsing it. The array includes the structure LSA_ENUMERATION_INFORMATION that contains only the SID:

typedef struct _LSA_ENUMERATION_INFORMATION {
PSID Sid;
} LSA_ENUMERATION_INFORMATION, *PLSA_ENUMERATION_INFORMATION;

Then the program converts the received SID into a standard username using the above-mentioned ConvertSidToStringSid() function and passes it to the LookupAccountSid() function to retrieve the username based on its SID. The last parameter specifies the enumerable type SID_NAME_USE in order to determine the type of the object. In most cases, it’s SidTypeUser (regular user), but you never know… Accordingly, I added short notes: “this is a user” and “this is a group.”

Examining privileges assigned to an object

The last, fifth, operating mode makes it possible to view privileges assigned to a specific account on a PC. Its beginning is rather unusual:

case '5':
std::wcout << L"[!] I'm not able to validate username and PC name. Make sure you enter the correct data." << std::endl;
Sleep(500);
std::wcout << L"[!] Starting" << std::endl;
if (InitMode5(argv[2], argv[3]) != 0) {
std::wcout << L"[-] InitMode 5 Error" << std::endl;
}
break;

You cannot validate the username and computer name without auxiliary RPC functions. Therefore, the program gives you 500 ms to review your input and make sure that it’s correct. This measure reduces the traffic and unnecessary noise. The main function is quite simple:

DWORD InitMode5(wchar_t* cCompName, wchar_t* cUsername) {
LSA_HANDLE hPolicy;
LSA_OBJECT_ATTRIBUTES lsaOA = { 0 };
LSA_UNICODE_STRING lsastrComputer = { 0 };
lsaOA.Length = sizeof(lsaOA);
lsastrComputer.Length = (USHORT)(lstrlen(cCompName) * sizeof(WCHAR));
lsastrComputer.MaximumLength = lsastrComputer.Length + sizeof(WCHAR);
lsastrComputer.Buffer = (PWSTR)cCompName;
NTSTATUS ntStatus = LsaOpenPolicy(&lsastrComputer, &lsaOA, POLICY_VIEW_LOCAL_INFORMATION | POLICY_LOOKUP_NAMES, &hPolicy);
ULONG lErr = LsaNtStatusToWinError(ntStatus);
if (lErr != ERROR_SUCCESS) {
if (lErr == 1722) {
std::wcout << L"[-] LsaOpenPolicy() failed: " << lErr << " | Is computer alive?" << std::endl;
return 1;
}
std::wcout << L"[-] LsaOpenPolicy() failed: " << lErr << std::endl;
return 1;
}
PSID UserSid;
DWORD sid_size = 0;
LPTSTR wSidStr = NULL;
DWORD domain_size = 0;
SID_NAME_USE sid_use;
if (!LookupAccountName(NULL, cUsername, NULL, &sid_size, NULL, &domain_size, &sid_use)) {
UserSid = (PSID)VirtualAlloc(NULL, sid_size, MEM_RESERVE | MEM_COMMIT, PAGE_READWRITE);
LPTSTR domain = NULL;
domain = (LPTSTR)VirtualAlloc(NULL, domain_size * sizeof(WCHAR), MEM_RESERVE | MEM_COMMIT, PAGE_READWRITE);
LookupAccountName(NULL, cUsername, UserSid, &sid_size, domain, &domain_size, &sid_use);
VirtualFree(domain, 0, MEM_RELEASE);
ConvertSidToStringSid(UserSid, &wSidStr);
std::wcout << L" [+] User SID: " << wSidStr << std::endl;
PrintTrusteePrivs(hPolicy, UserSid);
VirtualFree(UserSid, 0, MEM_RELEASE);
return 0;
} else {
std::wcout << L" [-] LookupAccountName() Error: " << GetLastError() << std::endl;
return 1;
}
return 1;
}

Everything revolves around the PrintTrusteePrivs()function created earlier. All you have to do is get an LSA policy handle on the target PC and then call LookupAccountName() to get the SID, which will be passed to the function that displays the existing privileges of an account. Isn’t this cool?

Conclusions

Overall, privileges in Windows represent a very broad and exciting topic. And Privileger can be easily expanded: you are welcome to elaborate, polish, and correct my divinely perfect code.

Should you have any questions or problems while using the tool, feel free to contact me. I will fix bugs (if any) and give a piece of advice.


Leave a Reply

XHTML: You can use these tags: <a href="" title=""> <abbr title=""> <acronym title=""> <b> <blockquote cite=""> <cite> <code> <del datetime=""> <em> <i> <q cite=""> <s> <strike> <strong>