Skip to content

Commit

Permalink
Add a Network Folder Canarytoken
Browse files Browse the repository at this point in the history
This new Canarytoken type gives users the ability to mount a WebDAV
endpoint, with unique credentials. If the WebDAV mount is browsed and
the file are access, we send a notification.

It relies on a WebDAV server written separately that is hosted as a
Cloudflare Worker. This feature will not run on self-hosted Canarytokens
instances.

Actual author: Jacob Torrey
  • Loading branch information
thinkst-marco committed Nov 21, 2024
1 parent 333e672 commit 1a1e68a
Show file tree
Hide file tree
Showing 26 changed files with 411 additions and 4 deletions.
5 changes: 5 additions & 0 deletions canarytokens/canarydrop.py
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@
from pathlib import Path
from urllib.parse import quote
from typing import Any, Literal, Optional, Union
from canarytokens.webdav import FsType

from pydantic import AnyHttpUrl, BaseModel, Field, parse_obj_as, root_validator

Expand Down Expand Up @@ -114,6 +115,10 @@ class Canarydrop(BaseModel):
file_name: Optional[str]
# CSS cloned site stuff
expected_referrer: Optional[str]
# WebDAV specific stuff
webdav_fs_type: Optional[FsType]
webdav_password: Optional[str]
webdav_server: Optional[str]

# AWS key specific stuff
aws_access_key_id: Optional[str]
Expand Down
5 changes: 5 additions & 0 deletions canarytokens/channel_http.py
Original file line number Diff line number Diff line change
Expand Up @@ -203,6 +203,11 @@ def render_POST(self, request: Request): # noqa: C901
canarydrop.add_canarydrop_hit(token_hit=token_hit)
self.dispatch(canarydrop=canarydrop, token_hit=token_hit)
return b"success"
elif canarydrop.type == TokenTypes.WEBDAV:
token_hit = Canarytoken._get_info_for_webdav(request)
canarydrop.add_canarydrop_hit(token_hit=token_hit)
self.dispatch(canarydrop=canarydrop, token_hit=token_hit)
return b"success"
elif canarydrop.type in [
TokenTypes.SLOW_REDIRECT,
TokenTypes.WEB_IMAGE,
Expand Down
47 changes: 47 additions & 0 deletions canarytokens/models.py
Original file line number Diff line number Diff line change
Expand Up @@ -283,6 +283,7 @@ class TokenTypes(str, enum.Enum):
ADOBE_PDF = "adobe_pdf"
WIREGUARD = "wireguard"
WINDOWS_DIR = "windows_dir"
WEBDAV = "webdav"
CLONEDSITE = "clonedsite"
CSSCLONEDSITE = "cssclonedsite"
CREDIT_CARD_V2 = "credit_card_v2"
Expand Down Expand Up @@ -327,6 +328,7 @@ def __str__(self) -> str:
TokenTypes.ADOBE_PDF: "Adobe PDF",
TokenTypes.WIREGUARD: "WireGuard",
TokenTypes.WINDOWS_DIR: "Windows folder",
TokenTypes.WEBDAV: "Network folder",
TokenTypes.CLONEDSITE: "JS cloned website",
TokenTypes.CSSCLONEDSITE: "CSS cloned website",
TokenTypes.QR_CODE: "QR code",
Expand Down Expand Up @@ -777,6 +779,21 @@ class WindowsDirectoryTokenRequest(TokenRequest):
token_type: Literal[TokenTypes.WINDOWS_DIR] = TokenTypes.WINDOWS_DIR


class WebDavTokenRequest(TokenRequest):
token_type: Literal[TokenTypes.WEBDAV] = TokenTypes.WEBDAV
webdav_fs_type: str

@validator("webdav_fs_type")
def check_webdav_fs_type(value: str):
from canarytokens.webdav import FsType

if not value.upper() in FsType.__members__.keys():
raise ValueError(
f"webdav_fs_type must be in the FsType enum. Given: {value}"
)
return value


class CreditCardV2TokenRequest(TokenRequest):
token_type: Literal[TokenTypes.CREDIT_CARD_V2] = TokenTypes.CREDIT_CARD_V2
cf_turnstile_response: Optional[str]
Expand All @@ -798,6 +815,7 @@ class CreditCardV2TokenRequest(TokenRequest):
ClonedWebTokenRequest,
CSSClonedWebTokenRequest,
WindowsDirectoryTokenRequest,
WebDavTokenRequest,
WebBugTokenRequest,
SlowRedirectTokenRequest,
MySQLTokenRequest,
Expand Down Expand Up @@ -1061,6 +1079,13 @@ class WindowsDirectoryTokenResponse(TokenResponse):
token_type: Literal[TokenTypes.WINDOWS_DIR] = TokenTypes.WINDOWS_DIR


class WebDavTokenResponse(TokenResponse):
webdav_fs_type: str
token_type: Literal[TokenTypes.WEBDAV] = TokenTypes.WEBDAV
webdav_password: str
webdav_server: str


class SMTPTokenResponse(TokenResponse):
token_type: Literal[TokenTypes.SMTP] = TokenTypes.SMTP
unique_email: Optional[EmailStr]
Expand Down Expand Up @@ -1123,6 +1148,7 @@ class CreditCardV2TokenResponse(TokenResponse):
MySQLTokenResponse,
WireguardTokenResponse,
WindowsDirectoryTokenResponse,
WebDavTokenResponse,
FastRedirectTokenResponse,
ClonedWebTokenResponse,
CSSClonedWebTokenResponse,
Expand Down Expand Up @@ -1339,6 +1365,7 @@ class AdditionalInfo(BaseModel):
mysql_client: Optional[dict[str, list[str]]]
r: Optional[list[str]]
l: Optional[list[str]]
file_path: Optional[list[str]]

def serialize_for_v2(self) -> dict:
data = json_safe_dict(self)
Expand Down Expand Up @@ -1657,6 +1684,19 @@ class WindowsDirectoryTokenHit(TokenHit):
src_data: Optional[dict]


class WebDavAdditionalInfo(BaseModel):
file_path: Optional[str]
useragent: Optional[str]

def serialize_for_v2(self) -> dict:
return self.dict()


class WebDavTokenHit(TokenHit):
token_type: Literal[TokenTypes.WEBDAV] = TokenTypes.WEBDAV
additional_info: Optional[WebDavAdditionalInfo]


class MsExcelDocumentTokenHit(TokenHit):
token_type: Literal[TokenTypes.MS_EXCEL] = TokenTypes.MS_EXCEL

Expand Down Expand Up @@ -1792,6 +1832,7 @@ class LegacyTokenHit(TokenHit):
FastRedirectTokenHit,
SMTPTokenHit,
WebBugTokenHit,
WebDavTokenHit,
MySQLTokenHit,
WireguardTokenHit,
QRCodeTokenHit,
Expand Down Expand Up @@ -1941,6 +1982,11 @@ class WindowsDirectoryTokenHistory(TokenHistory[WindowsDirectoryTokenHit]):
hits: List[WindowsDirectoryTokenHit] = []


class WebDavTokenHistory(TokenHistory[WebDavTokenHit]):
token_type: Literal[TokenTypes.WEBDAV] = TokenTypes.WEBDAV
hits: List[WebDavTokenHit] = []


class CustomBinaryTokenHistory(TokenHistory[CustomBinaryTokenHit]):
token_type: Literal[TokenTypes.SIGNED_EXE] = TokenTypes.SIGNED_EXE
hits: List[CustomBinaryTokenHit] = []
Expand Down Expand Up @@ -2041,6 +2087,7 @@ class LegacyTokenHistory(TokenHistory[LegacyTokenHit]):
SlowRedirectTokenHistory,
FastRedirectTokenHistory,
WebBugTokenHistory,
WebDavTokenHistory,
CustomBinaryTokenHistory,
WireguardTokenHistory,
QRCodeTokenHistory,
Expand Down
4 changes: 4 additions & 0 deletions canarytokens/settings.py
Original file line number Diff line number Diff line change
Expand Up @@ -118,6 +118,10 @@ class FrontendSettings(BaseSettings):
EXTEND_PASSWORD: Optional[SecretStr] = SecretStr("NoExtendPasswordFound")
EXTEND_CARD_NAME: Optional[str]
CLOUDFRONT_URL: Optional[HttpUrl]
CLOUDFLARE_ACCOUNT_ID: Optional[str] = ""
CLOUDFLARE_NAMESPACE: Optional[str] = ""
CLOUDFLARE_API_TOKEN: Optional[str] = ""
WEBDAV_SERVER: Optional[str] = ""
AZUREAPP_ID: Optional[str]
AZUREAPP_SECRET: Optional[str] # TODO: Figure out SecretStr with Azure secrets
CREDIT_CARD_TOKEN_ENABLED: bool = False
Expand Down
27 changes: 27 additions & 0 deletions canarytokens/tokens.py
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,8 @@
TokenTypes,
CreditCardV2TokenHit,
CreditCardV2AdditionalInfo,
WebDavTokenHit,
WebDavAdditionalInfo,
)
from canarytokens.credit_card_v2 import AnyCreditCardTrigger

Expand Down Expand Up @@ -528,6 +530,31 @@ def _get_info_for_cssclonedsite(request: Request):
}
return http_general_info, src_data

@staticmethod
def _get_info_for_webdav(request: Request):
http_general_info = Canarytoken._grab_http_general_info(request=request)
client_ip = request.getHeader("X-Client-Ip")
hit_time = datetime.utcnow().strftime("%s.%f")
hit_info = {
"additional_info": WebDavAdditionalInfo(
**{
"file_path": request.getHeader("X-Alert-Path"),
"useragent": http_general_info["useragent"],
}
),
"geo_info": queries.get_geoinfo(ip=client_ip),
"input_channel": INPUT_CHANNEL_HTTP,
"is_tor_relay": queries.is_tor_relay(client_ip),
"src_ip": client_ip,
"time_of_hit": hit_time,
}
return WebDavTokenHit(**hit_info)

@staticmethod
def _get_response_for_webdav(canarydrop: canarydrop.Canarydrop, request: Request):
request.setHeader("Content-Type", "image/gif")
return GIF

@staticmethod
def _get_response_for_cssclonedsite(
canarydrop: canarydrop.Canarydrop, request: Request
Expand Down
57 changes: 57 additions & 0 deletions canarytokens/webdav.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,57 @@
import json
import requests
from typing import Optional
from enum import Enum
from hashlib import sha1

from canarytokens.settings import FrontendSettings

settings = FrontendSettings()

ACCOUNT_ID = settings.CLOUDFLARE_ACCOUNT_ID
NAMESPACE_ID = settings.CLOUDFLARE_NAMESPACE


# Matches the keys in the worker's fs.js
class FsType(str, Enum):
TESTING = "testing"
SECURITY = "security"
DEFENSE = "defense"
MEDICAL = "medical"
IT = "it"


def generate_webdav_password(
token_id: str, server_domain: str = settings.DOMAINS[0]
) -> str:
return sha1((server_domain + token_id).encode()).hexdigest()


def insert_webdav_token(
password: str,
alert_url: str,
webdav_fs_type: Optional[FsType] = None,
custom_fs: Optional[str] = None,
) -> bool:
"""
Inserts a token config into the Cloudflare KV store
Returns: True if successful, False otherwise
"""
api_cred = settings.CLOUDFLARE_API_TOKEN
value = {"token_url": alert_url}

if webdav_fs_type is not None:
value["fs_template"] = f"{webdav_fs_type}"
if custom_fs is not None:
value["custom_fs"] = custom_fs

fd = {"value": json.dumps(value), "metadata": "{}"}
put_url = f"https://api.cloudflare.com/client/v4/accounts/{ACCOUNT_ID}/storage/kv/namespaces/{NAMESPACE_ID}/values/{password}"
res = requests.put(
put_url, files=fd, headers={"Authorization": "Bearer " + api_cred}
)
if res.status_code == 200:
return True
else:
print(res.text)
return False
47 changes: 47 additions & 0 deletions frontend/app.py
Original file line number Diff line number Diff line change
Expand Up @@ -135,6 +135,8 @@
TokenTypes,
WebBugTokenRequest,
WebBugTokenResponse,
WebDavTokenRequest,
WebDavTokenResponse,
WindowsDirectoryTokenRequest,
WindowsDirectoryTokenResponse,
WireguardTokenRequest,
Expand All @@ -150,6 +152,7 @@
EntraTokenStatus,
LEGACY_ENTRA_STATUS_MAP,
)
from canarytokens.webdav import generate_webdav_password, insert_webdav_token, FsType
from canarytokens.pdfgen import make_canary_pdf
from canarytokens.queries import (
add_canary_domain,
Expand Down Expand Up @@ -1604,6 +1607,50 @@ def _create_azure_id_token_response(
)


@create_response.register
def _(
token_request_details: WebDavTokenRequest, canarydrop: Canarydrop
) -> WebDavTokenResponse:
if not (
frontend_settings.WEBDAV_SERVER
and frontend_settings.CLOUDFLARE_ACCOUNT_ID
and frontend_settings.CLOUDFLARE_API_TOKEN
and frontend_settings.CLOUDFLARE_NAMESPACE
):
return JSONResponse(
{
"error_message": "This Canarytokens instance does not have the Network Folder Canarytoken enabled."
},
status_code=400,
)
canarydrop.webdav_fs_type = FsType(token_request_details.webdav_fs_type)
canarydrop.webdav_server = frontend_settings.WEBDAV_SERVER
queries.save_canarydrop(canarydrop=canarydrop)
canarydrop.webdav_password = generate_webdav_password(
canarydrop.canarytoken.value()
)
queries.save_canarydrop(canarydrop=canarydrop)
insert_webdav_token(
canarydrop.webdav_password,
canarydrop.get_url([canary_http_channel]),
canarydrop.webdav_fs_type,
)
return WebDavTokenResponse(
email=canarydrop.alert_email_recipient or "",
webhook_url=canarydrop.alert_webhook_url
if canarydrop.alert_webhook_url
else "",
token=canarydrop.canarytoken.value(),
token_url=canarydrop.get_url([canary_http_channel]),
auth_token=canarydrop.auth,
hostname=canarydrop.get_hostname(),
url_components=list(canarydrop.get_url_components()),
webdav_password=canarydrop.webdav_password,
webdav_server=canarydrop.webdav_server,
webdav_fs_type=canarydrop.webdav_fs_type,
)


@create_response.register
def _(
token_request_details: CMDTokenRequest, canarydrop: Canarydrop
Expand Down
4 changes: 4 additions & 0 deletions frontend/frontend.env.dist
Original file line number Diff line number Diff line change
Expand Up @@ -38,3 +38,7 @@ CANARY_WEB_IMAGE_UPLOAD_PATH=../uploads
#CANARY_CLOUDFRONT_URL=
#CANARY_AZUREAPP_ID=
#CANARY_AZUREAPP_SECRET=
#CANARY_CLOUDFLARE_NAMESPACE=
#CANARY_CLOUDFLARE_ACCOUNT_ID=
#CANARY_CLOUDFLARE_API_TOKEN=
#CANARY_WEBDAV_SERVER=
Binary file added frontend_vue/src/assets/token_icons/webdav.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
5 changes: 4 additions & 1 deletion frontend_vue/src/components/ModalContentHowToUse.vue
Original file line number Diff line number Diff line change
Expand Up @@ -74,9 +74,12 @@ li::before {
content: '\f0e7';
@apply text-green-500;
}
p :deep(li) {
list-style-type: disc;
}
p :deep(code) {
@apply bg-green-100 px-8 py-[2px] rounded-md mt-4;
overflow-wrap: anywhere;
}
</style>
6 changes: 4 additions & 2 deletions frontend_vue/src/components/ModalToken.vue
Original file line number Diff line number Diff line change
Expand Up @@ -271,10 +271,12 @@ async function handleGenerateToken(formValues: BaseFormValuesType) {
() => (modalType.value = ModalType.NewToken)
// Keep track of loaded components
).then(() => componentStack.value.push(modalType.value));
} catch (err) {
} catch (err: any) {
triggerSubmit.value = false;
isGenerateTokenError.value = true;
errorMessage.value = '';
if (err.response.data.error_message) {
errorMessage.value = err.response.data.error_message;
}
} finally {
triggerSubmit.value = false;
}
Expand Down
2 changes: 1 addition & 1 deletion frontend_vue/src/components/base/BaseFormSelect.vue
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
<template>
<label
:for="id"
class="mt-8 ml-4 font-semibold leading-3"
class="mt-8 ml-4 mb-8 font-semibold leading-3"
>{{ label }}</label
>
<p
Expand Down
2 changes: 2 additions & 0 deletions frontend_vue/src/components/constants.ts
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,7 @@ export const TOKENS_TYPE = {
CREDIT_CARD: 'cc',
PWA: 'pwa',
CREDIT_CARD_V2: 'credit_card_v2',
WEBDAV: 'webdav',
};

// unique keys to use in the frontend
Expand Down Expand Up @@ -89,6 +90,7 @@ export const INCIDENT_DETAIL_CUSTOM_LABELS = {
src_ip: 'Source IP',
loc: 'Location',
eventName: 'Event Name',
file_path: 'File Path Accessed',
};

export const TOKEN_CATEGORY = {
Expand Down
Loading

0 comments on commit 1a1e68a

Please sign in to comment.