Содержание статьи
О работе с привилегиями я писал в прошлой статье, но мы рассмотрели лишь обрывки кода, которые, как оказалось, сложно было превратить в полноценный проект. Поэтому, когда на очередном пентесте мне вновь потребовалось кодить все с нуля, я понял, что без готового инструмента не обойтись. И сделал 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»? Тогда этот вариант для тебя! Обрати внимание: этот способ подходит только для статей, опубликованных более двух месяцев назад.
Я уже участник «Xakep.ru»