Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Enable users to attack a domain using port scan and host alive #109

Merged
merged 3 commits into from
Dec 6, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
16 changes: 8 additions & 8 deletions kraken/src/api/handler/attacks.rs
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,6 @@ use actix_web::web::{Json, Path};
use actix_web::{delete, get, post, HttpResponse};
use chrono::{DateTime, Utc};
use futures::TryStreamExt;
use ipnetwork::IpNetwork;
use log::debug;
use rorm::conditions::{Condition, DynamicCollection};
use rorm::prelude::*;
Expand All @@ -27,7 +26,8 @@ use crate::modules::attacks::{
start_bruteforce_subdomains, start_certificate_transparency, start_dehashed_query,
start_dns_resolution, start_host_alive, start_service_detection, start_tcp_port_scan,
BruteforceSubdomainsParams, CertificateTransparencyParams, DehashedQueryParams,
DnsResolutionParams, HostAliveParams, ServiceDetectionParams, TcpPortScanParams,
DnsResolutionParams, DomainOrNetwork, HostAliveParams, ServiceDetectionParams,
TcpPortScanParams,
};

/// The settings of a subdomain bruteforce request
Expand Down Expand Up @@ -116,9 +116,9 @@ pub struct ScanTcpPortsRequest {
/// Leave empty to use a random leech
pub leech_uuid: Option<Uuid>,

/// The ip addresses / networks to scan
#[schema(value_type = Vec<String>, example = json!(["10.13.37.1", "10.13.37.2", "10.13.37.0/24"]))]
pub targets: Vec<IpNetwork>,
/// The ip addresses / networks or domains to scan
#[schema(value_type = Vec<String>, example = json!(["10.13.37.1", "10.13.37.0/24", "google.com"]))]
pub targets: Vec<DomainOrNetwork>,

/// List of single ports and port ranges
///
Expand Down Expand Up @@ -249,9 +249,9 @@ pub struct HostsAliveRequest {
/// Leave empty to use a random leech
pub leech_uuid: Option<Uuid>,

/// The ip addresses / networks to scan
#[schema(value_type = Vec<String>, example = json!(["10.13.37.1", "10.13.37.2", "10.13.37.0/24"]))]
pub targets: Vec<IpNetwork>,
/// The ip addresses / networks or domains to scan
#[schema(value_type = Vec<String>, example = json!(["10.13.37.1", "10.13.37.0/24", "google.com"]))]
pub targets: Vec<DomainOrNetwork>,

/// The time to wait until a host is considered down.
///
Expand Down
2 changes: 2 additions & 0 deletions kraken/src/api/swagger.rs
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ use crate::api::handler::{
oauth, oauth_applications, oauth_decisions, ports, services, settings, users, websocket,
wordlists, workspace_invitations, workspace_tags, workspaces,
};
use crate::modules;
use crate::modules::oauth::schemas as oauth_schemas;
use crate::modules::tls;
use crate::{chan, models};
Expand Down Expand Up @@ -267,6 +268,7 @@ impl Modify for SecurityAddon2 {
workspace_invitations::WorkspaceInvitationList,
chan::WsMessage,
chan::CertificateTransparencyEntry,
modules::attacks::DomainOrNetwork,
)),
modifiers(&SecurityAddon)
)]
Expand Down
28 changes: 23 additions & 5 deletions kraken/src/modules/aggregator/domain.rs
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ use log::warn;
use rorm::prelude::{ForeignModel, ForeignModelByField};
use rorm::{and, insert, query, update, FieldAccess, Model, Patch};
use tokio::sync::{mpsc, oneshot};
use tokio::task::JoinHandle;
use uuid::Uuid;

use crate::chan::GLOBAL;
Expand All @@ -16,7 +17,21 @@ pub async fn run_domain_aggregator(
)>,
) {
while let Some((data, tx)) = rx.recv().await {
let _ = tx.send(aggregate(data).await);
match aggregate(data).await {
Ok((uuid, None)) => {
let _ = tx.send(Ok(uuid));
}
Ok((uuid, Some(attack))) => {
// Await the attack in a new task to avoid blocking the aggregator
tokio::spawn(async move {
let _ = attack.await;
let _ = tx.send(Ok(uuid));
});
}
Err(error) => {
let _ = tx.send(Err(error));
}
}
}
}

Expand All @@ -30,9 +45,12 @@ struct DomainInsert {
workspace: ForeignModel<Workspace>,
}

async fn aggregate(data: DomainAggregationData) -> Result<Uuid, rorm::Error> {
async fn aggregate(
data: DomainAggregationData,
) -> Result<(Uuid, Option<JoinHandle<()>>), rorm::Error> {
let mut tx = GLOBAL.db.start_transaction().await?;

let mut attack_handle = None;
let uuid = if let Some((uuid, old_certainty)) =
query!(&mut tx, (Domain::F.uuid, Domain::F.certainty))
.condition(and![
Expand Down Expand Up @@ -62,7 +80,7 @@ async fn aggregate(data: DomainAggregationData) -> Result<Uuid, rorm::Error> {
.await?;

if let Ok(leech) = GLOBAL.leeches.random_leech() {
start_dns_resolution(
let (_, handle) = start_dns_resolution(
data.workspace,
data.user,
leech,
Expand All @@ -78,6 +96,7 @@ async fn aggregate(data: DomainAggregationData) -> Result<Uuid, rorm::Error> {
unreachable!("Workspace already used above")
}
})?;
attack_handle = Some(handle);
} else {
warn!(
"Couldn't resolve new domain \"{domain}\" automatically: No leech",
Expand All @@ -88,6 +107,5 @@ async fn aggregate(data: DomainAggregationData) -> Result<Uuid, rorm::Error> {
};

tx.commit().await?;

Ok(uuid)
Ok((uuid, attack_handle))
}
7 changes: 5 additions & 2 deletions kraken/src/modules/attacks/host_alive.rs
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
use crate::chan::{LeechClient, WsMessage, GLOBAL};
use crate::modules::attack_results::store_host_alive_check_result;
use crate::modules::attacks::{AttackContext, AttackError, HostAliveParams};
use crate::modules::attacks::{AttackContext, AttackError, DomainOrNetwork, HostAliveParams};
use crate::rpc::rpc_definitions::{HostsAliveRequest, HostsAliveResponse};

impl AttackContext {
Expand All @@ -10,9 +10,12 @@ impl AttackContext {
mut leech: LeechClient,
params: HostAliveParams,
) -> Result<(), AttackError> {
let targets =
DomainOrNetwork::resolve(self.workspace_uuid, self.user_uuid, &leech, &params.targets)
.await?;
let request = HostsAliveRequest {
attack_uuid: self.attack_uuid.to_string(),
targets: params.targets.into_iter().map(From::from).collect(),
targets: targets.into_iter().map(From::from).collect(),
timeout: params.timeout,
concurrent_limit: params.concurrent_limit,
};
Expand Down
90 changes: 85 additions & 5 deletions kraken/src/modules/attacks/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -8,25 +8,29 @@ mod host_alive;
mod service_detection;
mod tcp_port_scan;

use std::collections::HashSet;
use std::error::Error as StdError;
use std::net::IpAddr;

use chrono::Utc;
use dehashed_rs::{Query, ScheduledRequest};
use futures::{TryFuture, TryStreamExt};
use ipnetwork::IpNetwork;
use log::error;
use log::{debug, error};
use rorm::prelude::*;
use rorm::update;
use rorm::{and, query, update};
use serde::Deserialize;
use thiserror::Error;
use tokio::sync::mpsc;
use tokio::task::JoinHandle;
use tonic::{Response, Status, Streaming};
use utoipa::ToSchema;
use uuid::Uuid;

use crate::api::handler::attacks::PortOrRange;
use crate::chan::{LeechClient, WsMessage, GLOBAL};
use crate::models::{Attack, AttackType, InsertAttackError};
use crate::models::{Attack, AttackType, DomainHostRelation, InsertAttackError};
use crate::models::{Domain, DomainCertainty};
use crate::rpc::rpc_definitions::AddressConvError;

/// The parameters of a "bruteforce subdomains" attack
Expand Down Expand Up @@ -85,7 +89,7 @@ pub async fn start_dns_resolution(
/// The parameters of a "host alive" attack
pub struct HostAliveParams {
/// The ip addresses / networks to scan
pub targets: Vec<IpNetwork>,
pub targets: Vec<DomainOrNetwork>,

/// The time to wait until a host is considered down.
///
Expand All @@ -106,6 +110,7 @@ pub async fn start_host_alive(
Ok((
ctx.attack_uuid,
tokio::spawn(async move {
debug!("{:#?}", params.targets);
let result = ctx.host_alive(leech, params).await;
ctx.set_finished(result).await;
}),
Expand Down Expand Up @@ -201,7 +206,7 @@ pub async fn start_service_detection(
/// The parameters of a "tcp port scan" attack
pub struct TcpPortScanParams {
/// The ip addresses / networks to scan
pub targets: Vec<IpNetwork>,
pub targets: Vec<DomainOrNetwork>,

/// List of single ports and port ranges
pub ports: Vec<PortOrRange>,
Expand Down Expand Up @@ -358,3 +363,78 @@ pub enum AttackError {
#[error("{0}")]
Custom(Box<dyn StdError + Send + Sync>),
}
impl From<InsertAttackError> for AttackError {
fn from(value: InsertAttackError) -> Self {
match value {
InsertAttackError::DatabaseError(error) => Self::Database(error),
InsertAttackError::WorkspaceInvalid => Self::Custom(Box::new(value)),
}
}
}

/// Either an ip address / network or a domain name
#[derive(Debug, Clone, Deserialize, ToSchema)]
#[serde(untagged)]
pub enum DomainOrNetwork {
/// A ip address / network
#[schema(value_type = String, example = "10.13.37.10")]
Network(IpNetwork),

/// A domain name
#[schema(value_type = String, example = "kraken.test")]
Domain(String),
}
impl DomainOrNetwork {
/// Takes a list of [`DomainOrNetwork`] and produces it into a list of [`IpNetwork`]
/// by resolving the domains in a given workspace and starting implicit attacks if necessary.
pub async fn resolve(
workspace: Uuid,
user: Uuid,
leech: &LeechClient,
targets: &[Self],
) -> Result<HashSet<IpNetwork>, AttackError> {
let mut ips = HashSet::new();
for domain_or_network in targets {
match domain_or_network {
Self::Network(network) => {
ips.insert(*network);
}
Self::Domain(domain) => {
let certainty = query!(&GLOBAL.db, (Domain::F.certainty))
.condition(and![
Domain::F.workspace.equals(workspace),
Domain::F.domain.equals(domain)
])
.optional()
.await?;
if certainty != Some((DomainCertainty::Verified,)) {
let (_, attack) = start_dns_resolution(
workspace,
user,
leech.clone(),
DnsResolutionParams {
targets: vec![domain.to_string()],
concurrent_limit: 1,
},
)
.await?;
let _ = attack.await;
}

query!(&GLOBAL.db, (DomainHostRelation::F.host.ip_addr,))
.condition(and![
DomainHostRelation::F.workspace.equals(workspace),
DomainHostRelation::F.domain.domain.equals(domain)
])
.stream()
.try_for_each(|(ip,)| {
ips.insert(ip);
async { Ok(()) }
})
.await?;
}
}
}
Ok(ips)
}
}
7 changes: 5 additions & 2 deletions kraken/src/modules/attacks/tcp_port_scan.rs
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ use ipnetwork::IpNetwork;
use crate::api::handler::attacks::PortOrRange;
use crate::chan::{LeechClient, WsMessage, GLOBAL};
use crate::modules::attack_results::store_tcp_port_scan_result;
use crate::modules::attacks::{AttackContext, AttackError, TcpPortScanParams};
use crate::modules::attacks::{AttackContext, AttackError, DomainOrNetwork, TcpPortScanParams};
use crate::rpc::rpc_definitions;
use crate::rpc::rpc_definitions::shared::address::Address;
use crate::rpc::rpc_definitions::{shared, TcpPortScanRequest, TcpPortScanResponse};
Expand All @@ -17,9 +17,12 @@ impl AttackContext {
mut leech: LeechClient,
params: TcpPortScanParams,
) -> Result<(), AttackError> {
let targets =
DomainOrNetwork::resolve(self.workspace_uuid, self.user_uuid, &leech, &params.targets)
.await?;
let request = TcpPortScanRequest {
attack_uuid: self.attack_uuid.to_string(),
targets: params.targets.into_iter().map(From::from).collect(),
targets: targets.into_iter().map(From::from).collect(),
ports: params
.ports
.into_iter()
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7,10 +7,10 @@ import { toast } from "react-toastify";
import CollapseIcon from "../../../svg/collapse";
import ExpandIcon from "../../../svg/expand";
import { WORKSPACE_CONTEXT } from "../workspace";
import { PrefilledAttackParams } from "../workspace-attacks";
import { PrefilledAttackParams, TargetType } from "../workspace-attacks";
import { handleApiError } from "../../../utils/helper";

type WorkspaceAttacksHostAliveProps = { prefilled: PrefilledAttackParams };
type WorkspaceAttacksHostAliveProps = { prefilled: PrefilledAttackParams; targetType: TargetType | null };
type WorkspaceAttacksHostAliveState = {
target: string;
timeout: number;
Expand All @@ -29,16 +29,22 @@ export default class WorkspaceAttacksHostAlive extends React.Component<
super(props);

this.state = {
target: this.props.prefilled.ipAddr || "",
target:
(this.props.targetType === "domain" ? this.props.prefilled.domain : this.props.prefilled.ipAddr) || "",
timeout: 1000,
concurrentLimit: 50,
showAdvanced: false,
};
}

componentDidUpdate(prevProps: Readonly<WorkspaceAttacksHostAliveProps>) {
if (this.props.prefilled.ipAddr !== undefined && this.props.prefilled.ipAddr !== prevProps.prefilled.ipAddr)
this.setState({ target: this.props.prefilled.ipAddr });
if (this.props.targetType === "domain") {
if (this.props.prefilled.domain !== undefined && this.props.prefilled.domain !== prevProps.prefilled.domain)
this.setState({ target: this.props.prefilled.domain });
} else {
if (this.props.prefilled.ipAddr !== undefined && this.props.prefilled.ipAddr !== prevProps.prefilled.ipAddr)
this.setState({ target: this.props.prefilled.ipAddr });
}
}

startAttack() {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -8,10 +8,10 @@ import ExpandIcon from "../../../svg/expand";
import Checkbox from "../../../components/checkbox";
import { toast } from "react-toastify";
import { WORKSPACE_CONTEXT } from "../workspace";
import { PrefilledAttackParams } from "../workspace-attacks";
import { PrefilledAttackParams, TargetType } from "../workspace-attacks";
import { handleApiError } from "../../../utils/helper";

type WorkspaceAttacksPortScanTcpProps = { prefilled: PrefilledAttackParams };
type WorkspaceAttacksPortScanTcpProps = { prefilled: PrefilledAttackParams; targetType: TargetType | null };
type WorkspaceAttacksPortScanTcpState = {
ipAddInput: string;
showAdvanced: boolean;
Expand All @@ -36,7 +36,8 @@ export default class WorkspaceAttacksPortScanTcp extends React.Component<
super(props);

this.state = {
ipAddInput: this.props.prefilled.ipAddr || "",
ipAddInput:
(this.props.targetType === "domain" ? this.props.prefilled.domain : this.props.prefilled.ipAddr) || "",
showAdvanced: false,
interval: 100,
retries: 6,
Expand All @@ -48,8 +49,13 @@ export default class WorkspaceAttacksPortScanTcp extends React.Component<
}

componentDidUpdate(prevProps: Readonly<WorkspaceAttacksPortScanTcpProps>) {
if (this.props.prefilled.ipAddr !== undefined && this.props.prefilled.ipAddr !== prevProps.prefilled.ipAddr)
this.setState({ ipAddInput: this.props.prefilled.ipAddr });
if (this.props.targetType === "domain") {
if (this.props.prefilled.domain !== undefined && this.props.prefilled.domain !== prevProps.prefilled.domain)
this.setState({ ipAddInput: this.props.prefilled.domain });
} else {
if (this.props.prefilled.ipAddr !== undefined && this.props.prefilled.ipAddr !== prevProps.prefilled.ipAddr)
this.setState({ ipAddInput: this.props.prefilled.ipAddr });
}
}

startAttack() {
Expand Down
Loading