• XSS.stack #1 – первый литературный журнал от юзеров форума

Статья Управляем привилегиями в Windows

baykal

(L2) cache
Пользователь
Регистрация
16.03.2021
Сообщения
370
Реакции
838
Привилегии в Windows играют очень важную роль, так как администратор имеет возможность предоставить специальные права пользователям для решения их задач. В этой статье мы познакомимся с инструментом Privileger, который позволяет отыскивать в системе учетные записи с определенными привилегиями и менять привилегии у заданного аккаунта.

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

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


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

ДОБАВЛЯЕМ ПРИВИЛЕГИИ АККАУНТУ​

Итак, все, как и в любом другом проекте на языке С, начинается с функции main(), в которую прилетают все нужные параметры. После чего для обеспечения корректного вывода кириллических (и иных) символов дергаем setlocale(), выводим прекрасный ASCII-баннер и приступаем к валидации входных данных.
C:
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(), которая принимает имя пользователя, а также программное имя привилегии.
C:
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().
C:
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;
}
C:
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().
C:
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. Обнаружение по имени пользователя его SID.
  2. Генерация LSA_UNICODE_STRING с именем привилегии, которую нужно добавить.
  3. Вызов LsaAddAccountRights().
  4. Проверка успешности добавления привилегии.
Обнаружить SID пользователя по его имени можно с помощью упомянутой функции LookupAccountName(). Ее особенность заключается в том, что если в качестве буфера, куда должен упасть SID, передавать NULL, то функция просто вернет требуемый размер буфера под размещение в нем SID. Получаем размер, затем выделяем память под буфер и получаем SID.
C:
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;
Для преобразования структуры SID в строку, содержащую SID, можно вызвать ConvertSidToStringSid().

Имя привилегии должно быть записано в структуру LSA_UNICODE_STRING, выглядит вот так.
C:
typedef struct _LSA_UNICODE_STRING {
  USHORT Length;
  USHORT MaximumLength;
  PWSTR  Buffer;
} LSA_UNICODE_STRING, *PLSA_UNICODE_STRING;
Можно, например, заполнять структуру ручками, как сделал я в проекте, либо использовать отдельную функцию, как в моем инструменте для инжекта тикетов Kerberos.
C:
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);
Наконец, вызываем функцию LsaAddAccountRights().
C:
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;
    }
Условие if (bEnable) позволит использовать функцию AddUserPrivilege() не только для добавления, но и для удаления привилегий из аккаунта (эту операцию мы рассмотрим чуть позже). Сама функция по добавлению привилегии особо ничего не требует — хендл LSA, SID пользователя, имя привилегии, которые мы ей и передаем. Если привилегия успешно добавлена, дергаем PrintTrusteePrivs() — очередную функцию, которая выведет все привилегии, назначенные конкретному аккаунту.
C:
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);
}
Здесь с помощью функции LsaEnumerateAccountRights() мы получаем количество привилегий пользователя, а в буфер PLSA_UNICODE_STRING упадут их имена. После чего в цикле просто парсим их, дополнительно получая дружественные имена с помощью LookupPrivilegeDisplayNameW().

В принципе, вот так работает добавление привилегий аккаунту.

ЗАПУСКАЕМ ПРОЦЕСС С ПРИВИЛЕГИЕЙ​

Особенностью второго режима работы можно считать то, что нам придется проверять не только имя привилегии, но и путь, чтобы на каком‑нибудь пентесте в самый разгар работ не вывалилось веселое исключение. Проверку этих данных я реализовал в функции ValidatePathInfo().
C:
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;
}
Проверить существование пути (то есть убедиться, что файл, к которому пользователь указал путь, существует) удобнее всего с помощью PathFileExists. Функция работает проще некуда: вернет TRUE, если путь существует, FALSE — если нет.

Убедившись в корректности предоставленных данных, программа переходит к InitMode2():
C:
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;
}
Процесс можно разделить на несколько этапов:
  1. Привязка текущего токена процесса к токену текущего потока.
  2. Получение хендла на этот токен.
  3. Добавление привилегии в токен.
  4. Старт процесса с измененным токеном.
Итак, первые два шага достаточно простые. Нацепить токен процесса на токен текущего потока можно с помощью ImpersonateSelf(), затем получаем хендл на него через OpenThreadToken(). И вновь используется отдельная функция для добавления привилегии. Мне нравится подобная модульность, потому что можно взять из одного проекта функцию и просто скопировать ее в другой.
C:
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;
}
Сначала заводим специальную структуру, в которую занесем LUID привилегии. LUID — это некая интерпретация привилегии в системе, по которой винда понимает, что за привилегия перед ней. Получить LUID можно с помощью LookupPrivilegeValue(). Для добавления привилегии в токен передаем созданную структуру в функцию AdjustTokenPrivileges. Причем в нее можно передать сразу несколько привилегий. Например, чтобы сразу добавить SeDebugPrivilege и SeImpersonatePrivilege, но в нашем случае этого не требуется.

После чего поток управления возвращается в функцию InitMode2(), в которой мы вызываем функцию CreateProcessWithToken(), позволяющую создать процесс с токеном, куда мы добавили привилегии.
Код:
.\Privilegerx64.exe 2 C:\Windows\System32\cmd.exe SeDebugPrivilege
Успешный запуск процесса с нужными привилегиями


УДАЛЯЕМ ПРИВИЛЕГИЮ ИЗ АККАУНТА​

Иногда может потребоваться и обратный сценарий — нужно удалить привилегию из аккаунта. Начало стандартное, такое же, как и в первом режиме работы. То есть мы просто проверяем имя пользователя и привилегию на валидность. Далее получаем хендл на политику LSA текущего компа и дергаем AddUserPrivilege(). Помнишь эту функцию?
Код:
DWORD AddUserPrivilege(LSA_HANDLE hPolicy, LPWSTR wUsername, LPWSTR wPrivName, BOOL bEnable)
Последним параметром мы можем передать FALSE, что приведет к срабатыванию следующего условия.
Код:
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;
        }
    }
Как следствие, привилегия будет снята с указанного аккаунта с помощью вызова функции LsaRemoveAccountRights. Потом мы дергаем PrintTrusteePrivs(), что позволяет вывести новый список привилегий пользователя.
Код:
.\Privilegerx64.exe 3 Michael SeDebugPrivilege
img07.png


ИЩЕМ ОБЪЕКТЫ С ПРИВИЛЕГИЕЙ​

В этом случае инструменту нужно передать имя привилегии, которую мы ищем, а также имя компьютера, на котором мы ее ищем. Например, если нам нужно обнаружить все учетные записи, обладающие привилегией SeDebugPrivilege на компьютере WINPC, то запуск выполняется следующей командой:
Код:
.\Privilegerx64.exe 4 WINPC SeDebugPrivilege
img08.png


При запуске этого режима я решил не проверять имя компьютера, так как, если указать невалидное имя, сломается функция LsaOpenPolicy() — вернет код ошибки 1722.

img09.png

Сам функционал уместился в одну маленькую аккуратную функцию:
C:
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;
}
Сначала мы пытаемся получить хендл политики LSA на компьютере, имя которого было передано. Здесь все стандартно — LsaOpenPolicy(). После чего создаем специальные структуры для передачи их в функцию LsaEnumerateAccountsWithUserRight(). Результатом выполнения функции станет массив объектов, обладающих нужной привилегией. Причем опять же для передачи привилегии мы используем LSA_UNICODE_STRING. Получив массив объектов, начинаем его парсить. Он будет содержать структуру LSA_ENUMERATION_INFORMATION, внутри которой лежит лишь SID:
Код:
typedef struct _LSA_ENUMERATION_INFORMATION {
  PSID Sid;
} LSA_ENUMERATION_INFORMATION, *PLSA_ENUMERATION_INFORMATION;
Поэтому конвертируем в стандартное имя пользователя через уже известную нам функцию ConvertSidToStringSid(), а затем передаем в функцию LookupAccountSid() для получения имени пользователя по его SID. Последним параметром указывается перечисляемый тип SID_NAME_USE, с его помощью мы сможем определить тип объекта. Чаще всего это будет SidTypeUser (обычный пользователь), но мало ли что... В связи с этим я добавил также небольшие пометки, мол, тут — пользователь, а тут — группа.

СМОТРИМ ПРИВИЛЕГИИ ОБЪЕКТА​

Рассмотрим последний, пятый режим работы. Он позволяет находить привилегии, которыми обладает определенная учетная запись на компьютере. Начало тут не самое обычное:
Код:
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;
Не используя никакие вспомогательные RPC-функции, мы не сможем провалидировать имя пользователя и компьютера. Поэтому выделяем самим себе 500 мс, чтобы убедиться глазками, что все в порядке. Ну и опять же — меньше трафика, лишнего шума. Сама основная функция также достаточно проста:
C:
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;
}
Все крутится вокруг уже созданной функции PrintTrusteePrivs(). Нам остается лишь получить хендл политики LSA на компьютере, а затем дернуть LookupAccountName() для получения SID, который мы отдадим в функцию для вывода существующих привилегий у аккаунта. Круто, да?

ВЫВОДЫ​

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

Автор MichelleVermishelle
источник xakep.ru
 


Напишите ответ...
  • Вставить:
Прикрепить файлы
Верх