mirror of
https://github.com/Noratrieb/cluelessh.git
synced 2026-01-15 17:05:05 +01:00
agent
This commit is contained in:
parent
9b49e09983
commit
a52b6b37d7
19 changed files with 770 additions and 92 deletions
|
|
@ -49,8 +49,12 @@ enum ClientState {
|
|||
encryption_client_to_server: EncryptionAlgorithm,
|
||||
encryption_server_to_client: EncryptionAlgorithm,
|
||||
},
|
||||
ServiceRequest,
|
||||
Open,
|
||||
ServiceRequest {
|
||||
session_identifier: [u8; 32],
|
||||
},
|
||||
Open {
|
||||
session_identifier: [u8; 32],
|
||||
},
|
||||
}
|
||||
|
||||
impl ClientConnection {
|
||||
|
|
@ -229,6 +233,7 @@ impl ClientConnection {
|
|||
let kex_secret = mem::take(kex_secret).unwrap();
|
||||
let shared_secret = (kex_secret.exchange)(server_ephermal_key)?;
|
||||
|
||||
// The exchange hash serves as the session identifier.
|
||||
let hash = crypto::key_exchange_hash(
|
||||
client_ident,
|
||||
server_ident,
|
||||
|
|
@ -279,13 +284,15 @@ impl ClientConnection {
|
|||
false,
|
||||
);
|
||||
|
||||
debug!("Requestin ssh-userauth service");
|
||||
debug!("Requesting ssh-userauth service");
|
||||
self.packet_transport
|
||||
.queue_packet(Packet::new_msg_service_request(b"ssh-userauth"));
|
||||
|
||||
self.state = ClientState::ServiceRequest;
|
||||
self.state = ClientState::ServiceRequest {
|
||||
session_identifier: *h,
|
||||
};
|
||||
}
|
||||
ClientState::ServiceRequest => {
|
||||
ClientState::ServiceRequest { session_identifier } => {
|
||||
let mut accept = packet.payload_parser();
|
||||
let packet_type = accept.u8()?;
|
||||
if packet_type != numbers::SSH_MSG_SERVICE_ACCEPT {
|
||||
|
|
@ -297,9 +304,11 @@ impl ClientConnection {
|
|||
}
|
||||
|
||||
debug!("Connection has been opened successfully");
|
||||
self.state = ClientState::Open;
|
||||
self.state = ClientState::Open {
|
||||
session_identifier: *session_identifier,
|
||||
};
|
||||
}
|
||||
ClientState::Open => {
|
||||
ClientState::Open { .. } => {
|
||||
self.plaintext_packets.push_back(packet);
|
||||
}
|
||||
}
|
||||
|
|
@ -319,8 +328,11 @@ impl ClientConnection {
|
|||
self.packet_transport.queue_packet(packet);
|
||||
}
|
||||
|
||||
pub fn is_open(&self) -> bool {
|
||||
matches!(self.state, ClientState::Open)
|
||||
pub fn is_open(&self) -> Option<[u8; 32]> {
|
||||
match self.state {
|
||||
ClientState::Open { session_identifier } => Some(session_identifier),
|
||||
_ => None,
|
||||
}
|
||||
}
|
||||
|
||||
fn send_kexinit(&mut self, client_ident: Vec<u8>, server_ident: Vec<u8>) {
|
||||
|
|
|
|||
59
ssh-transport/src/key.rs
Normal file
59
ssh-transport/src/key.rs
Normal file
|
|
@ -0,0 +1,59 @@
|
|||
//! Operations on SSH keys.
|
||||
|
||||
use std::fmt::Display;
|
||||
|
||||
use base64::Engine;
|
||||
|
||||
use crate::parse::{self, ParseError, Parser, Writer};
|
||||
|
||||
pub enum SshPubkey {
|
||||
Ed25519 { public_key: [u8; 32] },
|
||||
}
|
||||
|
||||
impl SshPubkey {
|
||||
/// Parses an SSH public key from its wire encoding as specified in
|
||||
/// RFC4253, RFC5656, and RFC8709.
|
||||
pub fn from_wire_encoding(bytes: &[u8]) -> parse::Result<Self> {
|
||||
let mut p = Parser::new(bytes);
|
||||
let alg = p.utf8_string()?;
|
||||
|
||||
let k = match alg {
|
||||
"ssh-ed25519" => {
|
||||
let len = p.u32()?;
|
||||
if len != 32 {
|
||||
return Err(ParseError(format!("incorrect ed25519 len: {len}")));
|
||||
}
|
||||
let public_key = p.array::<32>()?;
|
||||
Self::Ed25519 { public_key }
|
||||
}
|
||||
_ => return Err(ParseError(format!("unsupported key type: {alg}"))),
|
||||
};
|
||||
Ok(k)
|
||||
}
|
||||
|
||||
pub fn to_wire_encoding(&self) -> Vec<u8> {
|
||||
let mut p = Writer::new();
|
||||
match self {
|
||||
Self::Ed25519 { public_key } => {
|
||||
p.string(b"ssh-ed25519");
|
||||
p.string(public_key);
|
||||
}
|
||||
}
|
||||
p.finish()
|
||||
}
|
||||
}
|
||||
|
||||
impl Display for SshPubkey {
|
||||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||
match self {
|
||||
Self::Ed25519 { .. } => {
|
||||
let encoded_pubkey = b64encode(&self.to_wire_encoding());
|
||||
write!(f, "ssh-ed25519 {encoded_pubkey}")
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn b64encode(bytes: &[u8]) -> String {
|
||||
base64::prelude::BASE64_STANDARD_NO_PAD.encode(bytes)
|
||||
}
|
||||
|
|
@ -1,5 +1,6 @@
|
|||
pub mod client;
|
||||
mod crypto;
|
||||
pub mod key;
|
||||
pub mod numbers;
|
||||
pub mod packet;
|
||||
pub mod parse;
|
||||
|
|
@ -20,7 +21,6 @@ pub enum SshStatus {
|
|||
|
||||
pub type Result<T, E = SshStatus> = std::result::Result<T, E>;
|
||||
|
||||
|
||||
pub trait SshRng {
|
||||
fn fill_bytes(&mut self, dest: &mut [u8]);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -5,7 +5,7 @@ use std::mem;
|
|||
|
||||
use tracing::{debug, trace};
|
||||
|
||||
use crate::crypto::{EncryptionAlgorithm, Keys, Plaintext, Session};
|
||||
use crate::crypto::{self, EncryptionAlgorithm, Keys, Plaintext, Session};
|
||||
use crate::parse::{NameList, Parser, Writer};
|
||||
use crate::Result;
|
||||
use crate::{numbers, peer_error};
|
||||
|
|
@ -347,19 +347,38 @@ impl RawPacket {
|
|||
}
|
||||
}
|
||||
|
||||
struct PacketParser {
|
||||
pub struct PacketParser {
|
||||
// The length of the packet.
|
||||
packet_length: Option<usize>,
|
||||
// The raw data *encrypted*, including the length.
|
||||
raw_data: Vec<u8>,
|
||||
done: bool,
|
||||
}
|
||||
impl PacketParser {
|
||||
fn new() -> Self {
|
||||
pub fn new() -> Self {
|
||||
Self {
|
||||
packet_length: None,
|
||||
raw_data: Vec::new(),
|
||||
done: false,
|
||||
}
|
||||
}
|
||||
|
||||
/// Parse a raw packet body out of a plaintext stream of bytes.
|
||||
/// # Returns
|
||||
/// - `Err()` - if the packet was invalid
|
||||
/// - `Ok(None)` - if the packet is incomplete and needs more data
|
||||
/// - `Ok(Some(consumed, all_data))` if a packet has been parsed.
|
||||
/// `consumed` is the amount of bytes from `bytes` that were actually consumed,
|
||||
/// `all_data` is the entire packet including the length.
|
||||
pub fn recv_plaintext_bytes(&mut self, bytes: &[u8]) -> Result<Option<(usize, Vec<u8>)>> {
|
||||
let Some((consumed, data)) = self.recv_bytes_inner(bytes, &mut crypto::Plaintext, 0)?
|
||||
else {
|
||||
return Ok(None);
|
||||
};
|
||||
self.done = true;
|
||||
Ok(Some((consumed, data.raw)))
|
||||
}
|
||||
|
||||
fn recv_bytes(
|
||||
&mut self,
|
||||
bytes: &[u8],
|
||||
|
|
@ -378,6 +397,11 @@ impl PacketParser {
|
|||
keys: &mut dyn Keys,
|
||||
next_seq_nr: u64,
|
||||
) -> Result<Option<(usize, RawPacket)>> {
|
||||
assert!(
|
||||
!self.done,
|
||||
"Passed bytes to packet parser even after it was completed"
|
||||
);
|
||||
|
||||
let mut consumed = 0;
|
||||
let packet_length = match self.packet_length {
|
||||
Some(packet_length) => {
|
||||
|
|
@ -460,7 +484,7 @@ impl ProtocolIdentParser {
|
|||
// The peer will not send any more information than this until we respond, so discord the rest of the bytes.
|
||||
let peer_ident = mem::take(&mut self.0);
|
||||
let peer_ident_string = String::from_utf8_lossy(&peer_ident);
|
||||
debug!(identification = %peer_ident_string, "Peer identifier");
|
||||
debug!(identification = %peer_ident_string.trim(), "Peer identifier");
|
||||
|
||||
Some(peer_ident)
|
||||
} else {
|
||||
|
|
|
|||
|
|
@ -1,7 +1,24 @@
|
|||
use core::str;
|
||||
use std::fmt::Debug;
|
||||
use std::fmt::{Debug, Display};
|
||||
|
||||
use crate::Result;
|
||||
use crate::SshStatus;
|
||||
|
||||
#[derive(Debug)]
|
||||
pub struct ParseError(pub String);
|
||||
impl Display for ParseError {
|
||||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||
f.write_str(&self.0)
|
||||
}
|
||||
}
|
||||
impl std::error::Error for ParseError {}
|
||||
|
||||
impl From<ParseError> for SshStatus {
|
||||
fn from(err: ParseError) -> Self {
|
||||
Self::PeerError(err.0)
|
||||
}
|
||||
}
|
||||
|
||||
pub type Result<T, E = ParseError> = std::result::Result<T, E>;
|
||||
|
||||
/// A simplified `byteorder` clone that emits client errors when the data is too short.
|
||||
pub struct Parser<'a>(&'a [u8]);
|
||||
|
|
@ -11,6 +28,10 @@ impl<'a> Parser<'a> {
|
|||
Self(data)
|
||||
}
|
||||
|
||||
pub fn has_data(&self) -> bool {
|
||||
!self.0.is_empty()
|
||||
}
|
||||
|
||||
pub fn u8(&mut self) -> Result<u8> {
|
||||
let arr = self.array::<1>()?;
|
||||
Ok(arr[0])
|
||||
|
|
@ -24,7 +45,7 @@ impl<'a> Parser<'a> {
|
|||
pub fn array<const N: usize>(&mut self) -> Result<[u8; N]> {
|
||||
assert!(N < 100_000);
|
||||
if self.0.len() < N {
|
||||
return Err(crate::peer_error!("packet too short"));
|
||||
return Err(ParseError(format!("packet too short")));
|
||||
}
|
||||
let result = self.0[..N].try_into().unwrap();
|
||||
self.0 = &self.0[N..];
|
||||
|
|
@ -33,10 +54,10 @@ impl<'a> Parser<'a> {
|
|||
|
||||
pub fn slice(&mut self, len: usize) -> Result<&'a [u8]> {
|
||||
if self.0.len() < len {
|
||||
return Err(crate::peer_error!("packet too short"));
|
||||
return Err(ParseError(format!("packet too short")));
|
||||
}
|
||||
if len > 100_000 {
|
||||
return Err(crate::peer_error!("bytes too long: {len}"));
|
||||
return Err(ParseError(format!("bytes too long: {len}")));
|
||||
}
|
||||
let result = &self.0[..len];
|
||||
self.0 = &self.0[len..];
|
||||
|
|
@ -48,7 +69,7 @@ impl<'a> Parser<'a> {
|
|||
match b {
|
||||
0 => Ok(false),
|
||||
1 => Ok(true),
|
||||
_ => Err(crate::peer_error!("invalid bool: {b}")),
|
||||
_ => Err(ParseError(format!("invalid bool: {b}"))),
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -70,7 +91,7 @@ impl<'a> Parser<'a> {
|
|||
pub fn utf8_string(&mut self) -> Result<&'a str> {
|
||||
let s = self.string()?;
|
||||
let Ok(s) = str::from_utf8(s) else {
|
||||
return Err(crate::peer_error!("name-list is invalid UTF-8"));
|
||||
return Err(ParseError(format!("name-list is invalid UTF-8")));
|
||||
};
|
||||
Ok(s)
|
||||
}
|
||||
|
|
@ -165,7 +186,7 @@ impl<'a> NameList<'a> {
|
|||
|
||||
impl Debug for NameList<'_> {
|
||||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||
self.0.fmt(f)
|
||||
Debug::fmt(&self.0, f)
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue