Remote Session Enumeration via Undocumented Windows APIs
What is qwinsta?
qwinsta
: Displays information about sessions on a Remote Desktop Session Host server. The list includes information not only about active sessions but also about other sessions that the server runs. (ref: MSFT Docs)
It is also possible to remotely enumerate user sessions via the /server:{hostname}
parameter. Despite the Microsoft documentation specifying this binary being related to Remote Desktop Sessions, Remote Desktop does not need to be enabled in order for the binary, and enumeration to succeed:
RDP Disabled
“Remote” Enumeration
Windows API Implementation
As with most native Windows binaries, qwinsta
leverages Windows API functions in order to retrieve session information from a host.
Disassembly
qwinsta
specifically utilizes the Windows Station (WinStation) API. We can find this out by simply plugging qwinsta.exe
into a disassembler:
However, for those knowledgeable individuals, you are probably also aware that there is another API that we can leverage for the same task- the Terminal Services API (also known as Remote Desktop Services API). harmj0y wrote a blog on implementing the functionality I’m going to be writing about using PowerShell. The blog written by harmj0y that I linked above leverages the RDS/WTS API, so for the purposes of education and not rehashing that work - I’ll use the other less documented API.
Key APIs
Interestingly enough, unlike the Terminal Services API, which can be used in C/C++ programs directly through the Windows header file wtsapi32.h
- qwinsta
relies on importing of the Dynamic Linked Library, winsta.dll
for function imports and there is no winsta32.h
or equivalent header file included in the Windows SDK that I could find. In the same disassembler, We can see these function imports:
- WinStationOpenServerW: Opens a handle to the specified terminal server.
- WinStationEnumerateW: Enumerates all sessions on the server.
- WinStationQueryInformationW: Retrieves information about a specified session.
- WinStationFreeMemory: Frees the memory allocated for the session information.
- WinStationCloseServer: Closes the handle to the server.
This is also the logical order for calling these APIs.
Specifying Target Host
WinStationOpenServerW
is undocumented by Microsoft, but I did find an implementation of winsta.h
in the Process Hacker docs (https://processhacker.sourceforge.io/doc/winsta_8h_source.html).
HANDLE
WINAPI
WinStationOpenServerW(
_In_ PWSTR ServerName
);
By this, we can validate that we will need to pass in a wchar_t
(the hostname). However, we can’t really use this header in its current state since we don’t have the function definitions. The function definitions are actually within winsta.dll
. Because of this, we need to take a few extra steps:
- Define all of our function prototypes e.g.:
WinStationOpenServerW
(`typedef HANDLE(WINAPI* LPFN_WinStationOpenServerW)(PWSTR);)
- Use
LoadLibrary
(https://learn.microsoft.com/en-us/windows/win32/api/libloaderapi/nf-libloaderapi-loadlibrarya) to get a handle to the WinSta DLL, then get the memory address of each function we’ll need to call. - Close all our handles when we’re done.
Example Code to Open Handle to Remote Host
The following code should accomplish four tasks:
- Load the WinSta DLL into memory
- Retrieve pointers for WinStationOpenServerW and WinStationCloseServer
- Open and Close Server Handle
- Close DLL handle
#include <Windows.h>
#include <iostream>
typedef HANDLE(WINAPI* LPFN_WinStationOpenServerW)(PWSTR);
typedef BOOLEAN(WINAPI* LPFN_WinStationCloseServer)(HANDLE);
int main() {
HINSTANCE hDLL = LoadLibrary(TEXT("winsta.dll"));
if (hDLL == NULL) {
std::cerr << "Failed to load winsta.dll\n";
return 1;
}
// Find Address of WinStationOpenServerW
LPFN_WinStationOpenServerW pfnWinStationOpenServerW =
(LPFN_WinStationOpenServerW)GetProcAddress(hDLL, "WinStationOpenServerW");
if (pfnWinStationOpenServerW == NULL) {
std::cerr << "Failed to find WinStationOpenServerW function\n";
FreeLibrary(hDLL);
return 1;
}
// Find Address of WinStationCloseServer
LPFN_WinStationCloseServer pfnWinStationCloseServer =
(LPFN_WinStationCloseServer)GetProcAddress(hDLL, "WinStationCloseServer");
if (pfnWinStationCloseServer == NULL) {
std::cerr << "Failed to find WinStationCloseServer function\n";
FreeLibrary(hDLL);
return 1;
}
wchar_t serverName[] = L"TARGET_HOST"; // Target Hostname
HANDLE hServer = pfnWinStationOpenServerW(serverName);
if (hServer == NULL) {
std::cerr << "Failed to open server\n";
FreeLibrary(hDLL);
return 1;
}
else {
std::wcout << L"Server Handle: " << hServer << std::endl;
}
// Close Server Handle
BOOLEAN result = pfnWinStationCloseServer(hServer);
// Close DLL Handle
FreeLibrary(hDLL);
return 0;
}
Query Sessions
Like above, we need to import the function necessary to query all sessions on the target. We accomplish this via WinStationEnumerateW.
Again, official documentation for this API is non-existent but we can review the Process Hacker code again to get the appropriate structure:
typedef BOOLEAN(WINAPI* LPFN_WinStationEnumerateW)(HANDLE, PSESSIONIDW*, PULONG);
This will introduce some types we do not currently have available. However, again - we can look to bring in the ones defined in the PH source, e.g.
typedef struct _SESSIONIDW {
union {
ULONG SessionId;
ULONG LogonId;
};
WINSTATIONNAME WinStationName;
WINSTATIONSTATECLASS State;
} SESSIONIDW, * PSESSIONIDW;
As we bring in each needed structure, you’ll find we need to bring in other needed structures that aren’t defined - this is a tedious process but if you go one by one, not too difficult.
API Call
Now that all required structures and functions are available, we can call WinStationEnumerateW
:
// Enumerate Server for Active Sessions, store IDs in PSESSIONIDW (SESSIONIDW array)
PSESSIONIDW pSessionIds = NULL;
ULONG count = 0;
BOOLEAN enumResult = pfnWinStationEnumerateW(hServer, &pSessionIds, &count);
We are interested in pSessionIds
, which holds an array of PSESSIONIDW
. This struct contains 4 pieces of information we care about for each session:
- SessionId / LogonId
- WinStationName
- State
In the context of this API call, WinStationName is simply the SessionName from the qwinsta output:
Keen eyes will see that qwinsta
also has variables not seen in the PSESSIONIDW
struct. For now, I decided not to go down that rabbit hole, since the goal is simply to enumerate usernames, and whether or not they have an active (State 0) session.
Enumerate Session Info
WinStationEnumerateW
returns a boolean, and so if we successfully make the function call, we can then proceed to the final step - enumerating individual session info. To do this, we emply one final API call to WinStationQueryInformationW
. As luck would have it, this one is documented here.
Implementing this one is simple, since pSessionIds
is an array or SESSIONIDW
, we can iterate through it and then access the struct variables within.
Running into a snag
According to the function prototype:
typedef BOOLEAN(WINAPI* LPFN_WinStationQueryInformationW)(HANDLE, ULONG, WINSTATIONINFOCLASS, PVOID, ULONG, PULONG);
We will return back a WINSTATIONINFOCLASS
for each session and this is where things got weird for me. Inspection of wintern.h
showed the following definitions:
typedef enum _WINSTATIONINFOCLASS {
WinStationInformation = 8
} WINSTATIONINFOCLASS;
typedef struct _WINSTATIONINFORMATIONW {
BYTE Reserved2[70];
ULONG LogonId;
BYTE Reserved3[1140];
} WINSTATIONINFORMATIONW, * PWINSTATIONINFORMATIONW;
Reserved2 and Reserved3 contained a byte array and I had no idea what that represented but because we know every session will have a username, the session name, and maybe some other information we can just output the byte strings and then attempt to derive meaning after. To do this, I wrote a simple routine to convert the byte arrays to ASCII representation and what I got back was promising!
Final Touches
So this brought me to a good spot, I knew I’d be able to retrieve at least the username and session state from these byte arrays. I crowd sourced the help of Grzegorz Tworek for his thoughts. Using some insight from him, I was able to come up with a reliable way of extracting out pieces of information I was interested in.
The final output being something I was really happy to get working:
Source code available on my github.
Thanks for Reading!