При­виле­гии в Windows игра­ют очень важ­ную роль, так как адми­нис­тра­тор име­ет воз­можность пре­дос­тавить спе­циаль­ные пра­ва поль­зовате­лям для решения их задач. В этой статье мы поз­накомим­ся с инс­тру­мен­том Privileger, который поз­воля­ет отыс­кивать в сис­теме учет­ные записи с опре­делен­ными при­виле­гиями и менять при­виле­гии у задан­ного акка­унта.

О работе с при­виле­гиями я писал в прош­лой статье, но мы рас­смот­рели лишь обрывки кода, которые, как ока­залось, слож­но было прев­ратить в пол­ноцен­ный про­ект. Поэто­му, ког­да на оче­ред­ном пен­тесте мне вновь пот­ребова­лось кодить все с нуля, я понял, что без готово­го инс­тру­мен­та не обой­тись. И сде­лал Privileger, который, к моему удив­лению, стал дос­таточ­но популяр­ным.

Релиз инструмента
Ре­лиз инс­тру­мен­та

В сегод­няшней статье мы раз­берем прин­цип работы это­го инс­тру­мен­та, а так­же про­бежим­ся по всем пяти режимам, которые поз­воля­ют:

  • до­бавить при­виле­гии локаль­ному акка­унту с помощью вызова лишь одной фун­кции. Рань­ше это мож­но было сде­лать толь­ко через GPO и, если мне не изме­няет память, еще тре­бова­лась перезаг­рузка хос­та, что не очень удоб­но;
  • за­пус­тить про­цесс, добавив в его токен кон­крет­ную при­виле­гию;
  • уда­лить при­виле­гию у акка­унта. Опять же рань­ше это выпол­нялось с помощью GPO — сей­час исполь­зует­ся вызов фун­кции;
  • об­наружить акка­унт с нуж­ной при­виле­гией на какой‑нибудь машине;
  • об­наружить при­виле­гии у акка­унта на какой‑нибудь машине.
 

Добавляем привилегии аккаунту

Итак, все, как и в любом дру­гом про­екте на язы­ке С, начина­ется с фун­кции main(), в которую при­лета­ют все нуж­ные парамет­ры. Пос­ле чего для обес­печения кор­рек­тно­го вывода кирил­личес­ких (и иных) сим­волов дер­гаем setlocale(), выводим прек­расный ASCII-бан­нер и прис­тупа­ем к валида­ции вход­ных дан­ных.

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;
}

Ес­ли тре­бует­ся исполь­зовать пер­вый режим работы, то есть добавить при­виле­гии акка­унту, поль­зователь дол­жен пре­дос­тавить сле­дующие вход­ные дан­ные:

  • 1 — режим работы;
  • имя поль­зовате­ля — кому навеши­вать при­виле­гию;
  • прог­рам­мное имя при­виле­гии — какую при­виле­гию добав­лять.

Прог­рам­мное имя при­виле­гии — это само имя при­виле­гии. Есть еще так называ­емое дру­жес­твен­ное имя — это ее опи­сание. Нап­ример, прог­рам­мное имя SeDebugPrivilege, а дру­жес­твен­ное — Отладка программ.

Итак, обра­щаем­ся к Privileger.

.\Privilegerx64.exe 1 Michael SeDebugPrivilege
Успешное добавление привилегии
Ус­пешное добав­ление при­виле­гии

Я пре­дус­мотрел про­вер­ку на валид­ность име­ни поль­зовате­ля, а так­же име­ни при­виле­гии, что­бы пре­дот­вра­тить оче­пят­ки. Про­вер­ка реали­зует­ся фун­кци­ей ValidateAccInfo(), которая при­нима­ет имя поль­зовате­ля, а так­же прог­рам­мное имя при­виле­гии.

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;
}
Указание неверного имени привилегии
Ука­зание невер­ного име­ни при­виле­гии
Неверное имя пользователя
Не­вер­ное имя поль­зовате­ля

Про­вер­ку име­ни поль­зовате­ля я сде­лал с помощью фун­кции LookupAccountName(). Сама по себе она слу­жит для получе­ния SID (security identifier) поль­зовате­ля по его име­ни, но нам нич­то не меша­ет исполь­зовать ее прос­то для про­вер­ки име­ни поль­зовате­ля, ведь если компь­ютер не обна­ружит поль­зовате­ля с таким име­нем, то и вызов фун­кции при­ведет к ошиб­ке.

Про­вер­ку прог­рам­мно­го име­ни при­виле­гии я так­же вынес в отдель­ную фун­кцию.

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;
}
}

Здесь алго­ритм схож: дер­гаем LookupPrivilegeValue(), если при­виле­гия есть — все ок, если нет — ошиб­ка.

Убе­див­шись, что пре­дос­тавлен­ные дан­ные вер­ны, инс­тру­мент вызыва­ет InitMode1(), которо­му так­же переда­ет имя поль­зовате­ля и имя при­виле­гии. Внут­ри этой фун­кции мы получа­ем хендл на LSA текуще­го компь­юте­ра (так как работа­ем с при­виле­гиями локаль­ного акка­унта) вызовом фун­кции GetPolicy().

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;
}

По­луча­ют хендл на полити­ку через фун­кцию LsaOpenPolicy. Единс­твен­ная осо­бен­ность работы с LSA сос­тоит в том, что у нее свои коды оши­бок, которые прос­то через GetLastError() не пой­мать. Нуж­но получать зна­чение NTSTATUS, которое воз­вра­щает каж­дая фун­кция, работа­ющая с LSA, а затем переда­вать это зна­чение в LsaNtStatusToWinError() для пре­обра­зова­ния в понят­ный челове­чес­кому гла­зу код ошиб­ки.

По коду ошиб­ки мож­но понять, что сло­малось, — заг­лядывай в офи­циаль­ную докумен­тацию.

Ес­ли сис­тема выдала нам кор­рек­тный хендл и мы не получи­ли ERROR_ACCESS_DENIED, перехо­дим к навеши­ванию при­виле­гии поль­зовате­лю, к фун­кции AddUserPrivilege().

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;
}

Продолжение доступно только участникам

Вариант 1. Присоединись к сообществу «Xakep.ru», чтобы читать все материалы на сайте

Членство в сообществе в течение указанного срока откроет тебе доступ ко ВСЕМ материалам «Хакера», позволит скачивать выпуски в PDF, отключит рекламу на сайте и увеличит личную накопительную скидку! Подробнее

Вариант 2. Открой один материал

Заинтересовала статья, но нет возможности стать членом клуба «Xakep.ru»? Тогда этот вариант для тебя! Обрати внимание: этот способ подходит только для статей, опубликованных более двух месяцев назад.


  • Подпишись на наc в Telegram!

    Только важные новости и лучшие статьи

    Подписаться

  • Подписаться
    Уведомить о
    0 комментариев
    Межтекстовые Отзывы
    Посмотреть все комментарии