Skip to content
Open
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
1,854 changes: 1,608 additions & 246 deletions rust/Cargo.lock

Large diffs are not rendered by default.

3 changes: 3 additions & 0 deletions rust/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -140,6 +140,9 @@ bzip2 = "0"
zstd = "0"
rustls-pki-types = { version = "1.14.0", features = ["std"] }
cidr = "0.3.2"
binrw = "0.15.1"
smb = "0.11.2"
sddl = "0.1.9"

[workspace]
members = ["crates/nasl-function-proc-macro"]
Expand Down
4 changes: 4 additions & 0 deletions rust/src/nasl/builtin/error.rs
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ use super::http::HttpError;
use super::isotime::IsotimeError;
use super::raw_ip::RawIpError;
use super::regex::RegexError;
use super::smb::SmbError;
use super::snmp::SnmpError;
use super::sys::SysError;
use super::{misc::MiscError, network::socket::SocketError, ssh::SshError, string::StringError};
Expand All @@ -29,6 +30,8 @@ pub enum BuiltinError {
#[error("{0}")]
Http(HttpError),
#[error("{0}")]
Smb(SmbError),
#[error("{0}")]
Notus(NotusError),
#[error("{0}")]
String(StringError),
Expand Down Expand Up @@ -97,6 +100,7 @@ builtin_error_variant!(SocketError, Socket);
builtin_error_variant!(CryptographicError, Cryptographic);
builtin_error_variant!(SshError, Ssh);
builtin_error_variant!(HttpError, Http);
builtin_error_variant!(SmbError, Smb);
builtin_error_variant!(IsotimeError, Isotime);
builtin_error_variant!(RegexError, Regex);
builtin_error_variant!(KBError, KB);
Expand Down
4 changes: 3 additions & 1 deletion rust/src/nasl/builtin/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@ mod preferences;
pub mod raw_ip;
mod regex;
mod report_functions;
mod smb;
mod snmp;
mod ssh;
mod string;
Expand Down Expand Up @@ -51,6 +52,7 @@ pub fn nasl_std_functions() -> Executor {
.add_set(host::Host)
.add_set(http::NaslHttp2::default())
.add_set(http::NaslHttp)
.add_set(smb::Smb::default())
.add_set(network::socket::SocketFns)
.add_set(network::network::Network)
.add_set(regex::RegularExpressions)
Expand All @@ -62,9 +64,9 @@ pub fn nasl_std_functions() -> Executor {
.add_set(sys::Sys)
.add_set(ssh::Ssh::default())
.add_set(find_service::FindService)
.add_set(wmi::Wmi)
.add_set(snmp::Snmp)
.add_set(cert::NaslCerts::default())
.add_set(wmi::Wmi)
.add_set(notus::NaslNotus::default());

executor.add_set(krb5::Krb5::default());
Expand Down
43 changes: 43 additions & 0 deletions rust/src/nasl/builtin/smb/error.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
// SPDX-FileCopyrightText: 2025 Greenbone AG
//
// SPDX-License-Identifier: GPL-2.0-or-later WITH x11vnc-openssl-exception

use std::{io, net::AddrParseError};

use thiserror::Error;

pub type Result<T> = std::result::Result<T, SmbError>;

#[derive(Debug, Error)]
pub enum SmbError {
#[error("Handle ID {0} not found.")]
SMBHandleIdNotFound(i32),
#[error("SMB error: {0}")]
Smb(String),
#[error("IO error during SMB: {0}")]
IO(io::ErrorKind),
#[error("Failed to parse host: {0}")]
InvalidHost(String),
#[error("SMB errror serializing response: {0}")]
SerializeError(String),
#[error("SMB query: {0}")]
SmbQuery(String),
}

impl From<io::Error> for SmbError {
fn from(value: io::Error) -> Self {
Self::IO(value.kind())
}
}

impl From<smb::Error> for SmbError {
fn from(value: smb::Error) -> Self {
Self::Smb(format!("{value}"))
}
}

impl From<AddrParseError> for SmbError {
fn from(value: AddrParseError) -> Self {
Self::InvalidHost(value.to_string())
}
}
160 changes: 160 additions & 0 deletions rust/src/nasl/builtin/smb/handle.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,160 @@
// SPDX-FileCopyrightText: 2026 Greenbone AG
//
// SPDX-License-Identifier: GPL-2.0-or-later WITH x11vnc-openssl-exception

use super::error::{Result, SmbError};
use smb::{
AdditionalInfo, Client, ClientConfig, FileAccessMask, FileCreateArgs, Resource, UncPath,
};
use std::{
collections::{HashMap, HashSet},
io::Cursor,
};
use tokio::sync::{Mutex, MutexGuard};

use binrw::BinWrite;
type BorrowedHandle<'a> = MutexGuard<'a, SmbHandle>;
pub type HandleId = i32;

// Note that the first session ID we will
// hand out is an arbitrary high number, this is only to help
// debugging.
pub const MIN_HANDLE_ID: HandleId = 9000;

#[derive(Default)]
pub struct SmbHandle {
/// SMB Client
handle: Client,
target_path: Option<UncPath>,
}

impl SmbHandle {
pub async fn new(
server: String,
share: String,
username: String,
password: String,
) -> Result<Self> {
let mut config = ClientConfig::default();
config.connection.auth_methods.ntlm = true;

let handle = Client::new(config);
let target_path = UncPath::new(&server)
.map_err(SmbError::from)?
.with_share(share.as_str())
.map_err(SmbError::from)?;

handle
.share_connect(&target_path, &username, password)
.await
.map_err(SmbError::from)?;

Ok(Self {
handle,
target_path: Some(target_path),
})
}

pub async fn disconnect(&mut self) -> Result<()> {
self.handle.close().await.map_err(SmbError::from)
}

pub async fn get_full_info(&self, filename: String) -> Result<String> {
// Safe to unwrap because there is always a server and share.
let target_path = self
.target_path
.clone()
.unwrap()
.with_path(filename.as_str());

// Define read access mask
let file_open_args =
FileCreateArgs::make_open_existing(FileAccessMask::new().with_generic_read(true));

let resource = self.handle.create_file(&target_path, &file_open_args).await;

// From openvas-smb the query secinfo flags are 0x7 which means the following three.
let additional_info = AdditionalInfo::new()
.with_owner_security_information(true)
.with_group_security_information(true)
.with_dacl_security_information(true);

let response = match resource {
Ok(Resource::File(f)) => {
let r = f
.query_security_info(additional_info)
.await
.map_err(|e| SmbError::SmbQuery(e.to_string()))?;
f.close().await.map_err(|e| SmbError::Smb(e.to_string()))?;
r
}
Ok(Resource::Directory(d)) => {
let r = d
.query_security_info(additional_info)
.await
.map_err(|e| SmbError::SmbQuery(e.to_string()))?;
d.close().await.map_err(|e| SmbError::Smb(e.to_string()))?;
r
}
Ok(Resource::Pipe(p)) => {
let r = p
.query_security_info(additional_info)
.await
.map_err(|e| SmbError::SmbQuery(e.to_string()))?;
p.close().await.map_err(|e| SmbError::Smb(e.to_string()))?;
r
}
Err(e) => return Err(SmbError::Smb(e.to_string())),
};

// convert response to SDDL format
let mut writer = Cursor::new(Vec::new());
response
.write(&mut writer)
.map_err(|e| SmbError::SerializeError(e.to_string()))?;
let raw_sd = writer.into_inner();
let sd = sddl::SecurityDescriptor::from_bytes(&raw_sd)
.map_err(|e| SmbError::SerializeError(e.to_string()))?;
let ret = format!("{}", sd);

Ok(ret)
}
}

#[derive(Default)]
pub struct SmbHandles {
handles: HashMap<HandleId, Mutex<SmbHandle>>,
}

impl SmbHandles {
pub async fn get_by_id(&self, id: HandleId) -> Result<BorrowedHandle<'_>> {
Ok(self
.handles
.get(&id)
.ok_or(SmbError::SMBHandleIdNotFound(id))?
.lock()
.await)
}

/// Return the next available session ID
pub fn next_handle_id(&self) -> Result<HandleId> {
let taken_ids: HashSet<_> = self.handles.keys().collect();
if taken_ids.is_empty() {
Ok(MIN_HANDLE_ID)
} else {
let max_val = **taken_ids.iter().max().unwrap() + 1;
Ok((MIN_HANDLE_ID..=max_val)
.find(|id| !taken_ids.contains(id))
.unwrap())
}
}

pub fn insert(&mut self, handle_id: HandleId, handle: Mutex<SmbHandle>) {
self.handles.insert(handle_id, handle);
}

pub fn remove(&mut self, handle_id: HandleId) -> Result<()> {
self.handles.remove(&handle_id);
Ok(())
}
}
119 changes: 119 additions & 0 deletions rust/src/nasl/builtin/smb/mod.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,119 @@
// SPDX-FileCopyrightText: 2025 Greenbone AG
//
// SPDX-License-Identifier: GPL-2.0-or-later WITH x11vnc-openssl-exception

mod error;
mod handle;
use crate::nasl::prelude::*;
use std::net::IpAddr;
use std::process::Command;
use std::str::FromStr;

use dns_lookup::lookup_addr;

use tokio::sync::Mutex;

pub use error::{Result, SmbError};
pub use handle::SmbHandles as Smb;
use handle::{HandleId, SmbHandle};

const SMB_LIB_IMPLEMENTATION_VERSION: &str = "0.0.1";

#[nasl_function]
fn smb_versioninfo() -> String {
SMB_LIB_IMPLEMENTATION_VERSION.to_string()
}

impl Smb {
#[nasl_function(named(username, password, share))]
async fn smb_connect(
&mut self,
ctx: &ScanCtx<'_>,
username: String,
password: String,
share: String,
) -> Result<HandleId> {
let server = ctx.target().ip_addr();
let id = self.next_handle_id()?;

let handle =
Mutex::new(SmbHandle::new(server.to_string(), share, username, password).await?);
self.insert(id, handle);
Ok(id)
}

#[nasl_function(named(smb_handle))]
async fn smb_close(&mut self, smb_handle: i32) -> Result<()> {
{
let mut handle = self.get_by_id(smb_handle).await?;
handle.disconnect().await?;
}
Ok(self.remove(smb_handle)?)
}

#[nasl_function(named(smb_handle, filename))]
async fn smb_file_sddl(&mut self, smb_handle: i32, filename: String) -> Result<String> {
let handle = self.get_by_id(smb_handle).await?;

Ok(handle.get_full_info(filename).await?)
}
}

#[nasl_function(named(host, username, password, realm, kdc, cmd))]
fn nasl_win_cmd_exec(
ctx: &ScanCtx<'_>,
host: Option<String>,
username: String,
password: String,
realm: Option<String>,
kdc: Option<String>,
cmd: String,
) -> Result<Option<String>> {
let host = if let Some(host) = host {
IpAddr::from_str(&host)?
} else {
ctx.target().ip_addr()
};

let host = if kdc.is_some() {
lookup_addr(&host)?
} else {
host.to_string()
};

let username = username.replacen("\\", "/", 1);
let realm = realm.unwrap_or_default();
let target = if username.contains("/") {
format!("{username}:{password}@{host}")
} else {
format!("{realm}/{username}:{password}@{host}")
};

// Construct the command
let mut output = Command::new("impacket-wmiexec");
if let Some(ref krb5) = kdc {
let first_kdc = if let Some(i) = krb5.find(",") {
&krb5[..i]
} else {
krb5
};
output.arg("-k").arg("-dc-ip").arg(first_kdc);
}
output.arg(target).arg(cmd);

let child = output.spawn()?;

let ret = child.wait_with_output()?;
Ok(Some(String::from_utf8_lossy(&ret.stdout).to_string()))
}

function_set! {
Smb,
(
(Smb::smb_connect, "smb_connect"),
(Smb::smb_close, "smb_close"),
(Smb::smb_file_sddl, "smb_file_SDDL"),
(nasl_win_cmd_exec, "win_cmd_exec"),
smb_versioninfo,
)
}
Loading