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

[ARC-144] add if condition for HTTP target, allow winrm connection #178

Closed
2 changes: 1 addition & 1 deletion .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -77,7 +77,7 @@ jobs:
- uses: actions/checkout@v3

- name: Test
run: cargo test --manifest-path ${{ matrix.manifest }} ${{ matrix.additional-args }}
run: cargo test --manifest-path ${{ matrix.manifest }} ${{ matrix.additional-args }} --test *
irvingoujAtDevolution marked this conversation as resolved.
Show resolved Hide resolved

wasm:
name: WASM target
Expand Down
8 changes: 6 additions & 2 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

43 changes: 30 additions & 13 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -11,14 +11,8 @@ description = "A Rust implementation of the Security Support Provider Interface
keywords = ["ntlm", "auth", "sspi", "windows", "kerberos"]

[workspace]
members = [
"ffi",
"ffi/symbol-rename-macro",
]
exclude = [
"tools/sspi-ffi-attacker",
"tools/wasm-testcompile",
]
members = ["ffi", "ffi/symbol-rename-macro"]
exclude = ["tools/sspi-ffi-attacker", "tools/wasm-testcompile"]

[features]
default = []
Expand Down Expand Up @@ -47,7 +41,11 @@ lazy_static = "1.4"
serde = "1"
serde_derive = "1"
url = "2.4"
reqwest = { version = "0.11", features = ["blocking", "rustls-tls", "rustls-tls-native-roots"], optional = true, default-features = false }
reqwest = { version = "0.11", features = [
"blocking",
"rustls-tls",
"rustls-tls-native-roots",
], optional = true, default-features = false }

picky = { version = "7.0.0-rc.8", default-features = false }
picky-krb = "0.8"
Expand All @@ -61,17 +59,32 @@ trust-dns-resolver = { version = "0.23", optional = true }
portpicker = { version = "0.1", optional = true }
num-bigint-dig = "0.8"
tracing = "0.1"
rustls = { version = "0.21", features = ["dangerous_configuration"], optional = true }
rustls = { version = "0.21", features = [
"dangerous_configuration",
], optional = true }
Copy link
Member

@CBenoit CBenoit Oct 6, 2023

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Please, do you think you could configure your editor to not automatically re-format all Cargo.toml you touch? It would be preferable so we don’t end up with noise, and don’t deviate too much from other Rust projects.

Most Rust projects typically does not use this formatting. This is well explained by the style guide from the Rust compiler: "[f]or array values, such as a list of features, put the entire list on the same line as the key, if it fits".

I would prefer we wait for official support in rustfmt before we apply such automatic formatting.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

updated, both the my disabled my vscode formatter and the file. Do you have a recommanded toml formatter tool for this?

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Sadly there is currently no blessed formatter for Cargo.toml as of today (but that would be rust-lang/rustfmt#5240 when it’s ready)

zeroize = { version = "1.6", features = ["zeroize_derive"] }
tokio = { version = "1.32", features = ["time", "rt"], optional = true }
pcsc = { version = "2.8", optional = true }
async-recursion = "1.0.5"

[target.'cfg(windows)'.dependencies]
winreg = "0.51"
winapi = { version = "0.3", features = ["std", "sspi", "rpcdce", "impl-default", "timezoneapi", "wincrypt"] }
windows = { version = "0.51", features = [ "Win32_Foundation", "Win32_NetworkManagement_Dns"] }
windows-sys = { version = "0.48", features = ["Win32_Security_Cryptography", "Win32_Foundation"] }
winapi = { version = "0.3", features = [
"std",
"sspi",
"rpcdce",
"impl-default",
"timezoneapi",
"wincrypt",
] }
windows = { version = "0.51", features = [
"Win32_Foundation",
"Win32_NetworkManagement_Dns",
] }
windows-sys = { version = "0.48", features = [
"Win32_Security_Cryptography",
"Win32_Foundation",
] }

[target.'cfg(any(target_os = "macos", target_os = "ios"))'.dependencies]
async-dnssd = "0.5"
Expand All @@ -82,3 +95,7 @@ tokio = { version = "1.32", features = ["time", "rt"] }
static_assertions = "1"
whoami = "1.4"
picky = { version = "7.0.0-rc.8", features = ["x509"] }
base64 = "0.21.4"
hyper = "0.14.27"
tracing-log = "0.1.3"
irvingoujAtDevolution marked this conversation as resolved.
Show resolved Hide resolved
tracing-subscriber = "0.3.17"
140 changes: 140 additions & 0 deletions examples/kerberos.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,140 @@
use base64::Engine;
use hyper::header::{
ACCEPT, ACCEPT_ENCODING, ACCEPT_LANGUAGE, AUTHORIZATION, CONNECTION, CONTENT_LENGTH, HOST, USER_AGENT,
};
use hyper::StatusCode;
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Use the items re-exported by reqwest and remove the dependency on hyper

Suggested change
use hyper::header::{
ACCEPT, ACCEPT_ENCODING, ACCEPT_LANGUAGE, AUTHORIZATION, CONNECTION, CONTENT_LENGTH, HOST, USER_AGENT,
};
use hyper::StatusCode;
use reqwest::header::{
ACCEPT, ACCEPT_ENCODING, ACCEPT_LANGUAGE, AUTHORIZATION, CONNECTION, CONTENT_LENGTH, HOST, USER_AGENT,
};
use reqwest::StatusCode;

use sspi::builders::EmptyInitializeSecurityContext;
use sspi::{
AcquireCredentialsHandleResult, ClientRequestFlags, CredentialsBuffers, DataRepresentation,
InitializeSecurityContextResult, KerberosConfig, Negotiate, SecurityBuffer, SecurityBufferType, SecurityStatus,
Sspi,
};
use sspi::{Kerberos, SspiImpl};
use std::error::Error;
use tracing::debug;
use tracing_log::log::info;
irvingoujAtDevolution marked this conversation as resolved.
Show resolved Hide resolved
const TARGET_NAME: &'static str = "HTTP/your-server-name.your.domain";
irvingoujAtDevolution marked this conversation as resolved.
Show resolved Hide resolved
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Replace this with an environment variable as well, loaded at the beginning of main


fn main() -> Result<(), Box<dyn Error + Send + Sync>> {
tracing_subscriber::fmt()
.with_max_level(tracing::Level::DEBUG) // Adjust level as needed
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Use EnvFilter

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

udpated

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Marking as unresolved as you do not appear to be using EnvFilter yet

.init();
let kdc_url = "kdc.your.domain:88".to_string();
irvingoujAtDevolution marked this conversation as resolved.
Show resolved Hide resolved
let client_hostname = whoami::hostname();
let kerberos_config = KerberosConfig::new(&kdc_url, client_hostname.clone());
let mut kerberos = Kerberos::new_client_from_config(kerberos_config).unwrap();

let mut acq_creds_handle_result = get_cred_handle(&mut kerberos);

let mut input = vec![SecurityBuffer::new(Vec::new(), SecurityBufferType::Token)];
let mut output = vec![SecurityBuffer::new(Vec::new(), SecurityBufferType::Token)];

loop {
match step(
&mut kerberos,
&mut acq_creds_handle_result.credentials_handle,
&mut input,
&mut output,
) {
Err(e) => panic!("error steping {:?}", e),
Ok(result) => {
let status_code = process_authentication(&mut input, &mut output)?;
if status_code == StatusCode::OK {
info!("connection authenticated");
break Ok(());
}

if result.status == SecurityStatus::Ok && status_code != StatusCode::OK {
panic!("connection authentication failed");
}
}
}
}
}

fn get_cred_handle(kerberos: &mut Kerberos) -> AcquireCredentialsHandleResult<Option<CredentialsBuffers>> {
let username = "user@your.domain".to_string();
let password = "user's_password".to_string();
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Idem, let the user configure these using an environment variable. Load the value at the beginning of the program, in main.

let identity = sspi::AuthIdentity {
username,
password: password.into(),
domain: None,
};
let acq_creds_handle_result = kerberos
.acquire_credentials_handle()
.with_credential_use(sspi::CredentialUse::Outbound)
.with_auth_data(&identity.into())
.execute()
.expect("AcquireCredentialsHandle resulted in error");
acq_creds_handle_result
}

fn process_authentication(
input: &mut Vec<SecurityBuffer>,
output: &mut Vec<SecurityBuffer>,
) -> Result<StatusCode, Box<dyn std::error::Error + Send + Sync>> {
let output_token_in_binary = &output[0].buffer;
let base_64_token = base64::engine::general_purpose::STANDARD.encode(output_token_in_binary);
let server_result = send_http(base_64_token)?;
debug!("server responde = {:?}", server_result);
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Typo

Suggested change
debug!("server responde = {:?}", server_result);
debug!("server response = {:?}", server_result);

let www_authenticate = server_result
.headers()
.get("www-authenticate")
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

.ok_or("www-authentication header not found")?;
let server_token = www_authenticate.to_str().unwrap().replace("Negotiate ", "");
if server_token.len() <= 5 {
panic!("server token not found");
}
let decoded_new_token = base64::engine::general_purpose::STANDARD.decode(server_token).unwrap();
clear(input);
clear(output);
input[0].buffer = decoded_new_token;

Ok(server_result.status())
}

fn clear(buf: &mut Vec<SecurityBuffer>) {
buf[0].buffer.clear();
}

fn send_http(negotiate_token: String) -> Result<reqwest::blocking::Response, Box<dyn Error + Send + Sync>> {
let client = reqwest::blocking::Client::new();
let resp = client
.post("http://your-server-name.your.domain:5985/wsman")
.header(AUTHORIZATION, format!("Negotiate {}", negotiate_token))
.header(HOST, "our-server-name.your.domain:5985")
.header(CONNECTION, "keep-alive")
.header(CONTENT_LENGTH, "0")
.header(
USER_AGENT,
"Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:109.0) Gecko/20100101 Firefox/118.0",
)
.header(ACCEPT, "*/*")
.header(ACCEPT_ENCODING, "gzip, deflate")
.header(ACCEPT_LANGUAGE, "en-US,en;q=0.9")
.send()?;

Ok(resp)
}

fn step(
kerberos: &mut Kerberos,
credential_handle: &mut <Kerberos as SspiImpl>::CredentialsHandle,
input_buffer: &mut Vec<sspi::SecurityBuffer>,
output_buffer: &mut Vec<sspi::SecurityBuffer>,
) -> Result<InitializeSecurityContextResult, Box<dyn std::error::Error>> {
output_buffer[0].buffer.clear();
let mut builder = EmptyInitializeSecurityContext::<<Negotiate as SspiImpl>::CredentialsHandle>::new()
.with_credentials_handle(credential_handle)
.with_context_requirements(ClientRequestFlags::MUTUAL_AUTH)
.with_target_data_representation(DataRepresentation::Native)
.with_target_name(TARGET_NAME)
.with_input(input_buffer)
.with_output(output_buffer);

let result = kerberos
.initialize_security_context_impl(&mut builder)
.resolve_with_default_network_client()?;

Ok(result)
}
27 changes: 27 additions & 0 deletions src/kerberos/client/generators.rs
Original file line number Diff line number Diff line change
Expand Up @@ -572,6 +572,33 @@ pub fn generate_neg_ap_req(ap_req: ApReq, mech_id: oid::ObjectIdentifier) -> Res
}))
}

pub fn generate_neg_ap_req_plain(
ap_req: ApReq,
mech_id: oid::ObjectIdentifier,
) -> Result<ApplicationTag0<GssApiNegInit>> {
let krb_blob: ApplicationTag<_, 0> = ApplicationTag(KrbMessage {
krb5_oid: ObjectIdentifierAsn1::from(mech_id),
krb5_token_id: AP_REQ_TOKEN_ID,
krb_msg: ap_req,
});

let data = ExplicitContextTag0::from(NegTokenInit {
mech_types: Optional::from(Some(ExplicitContextTag0::from(get_mech_list()))),
req_flags: Optional::from(None),
mech_token: Optional::from(Some(ExplicitContextTag2::from(OctetStringAsn1::from(
picky_asn1_der::to_vec(&krb_blob)?,
)))),
mech_list_mic: Optional::from(None),
});

let gss_req = GssApiNegInit {
oid: ObjectIdentifierAsn1::from(oids::spnego()),
neg_token_init: data,
};

Ok(ApplicationTag0(gss_req))
}

pub fn generate_final_neg_token_targ(mech_list_mic: Option<Vec<u8>>) -> NegTokenTarg1 {
NegTokenTarg1::from(NegTokenTarg {
neg_result: Optional::from(Some(ExplicitContextTag0::from(Asn1RawDer(ACCEPT_COMPLETE.to_vec())))),
Expand Down
Loading
Loading