mirror of
https://github.com/Noratrieb/cluelessh.git
synced 2026-01-14 16:35:06 +01:00
move
This commit is contained in:
parent
7ac2ef4194
commit
de8f5dde21
34 changed files with 10 additions and 15 deletions
10
lib/ssh-protocol/Cargo.toml
Normal file
10
lib/ssh-protocol/Cargo.toml
Normal file
|
|
@ -0,0 +1,10 @@
|
|||
[package]
|
||||
name = "ssh-protocol"
|
||||
version = "0.1.0"
|
||||
edition = "2021"
|
||||
|
||||
[dependencies]
|
||||
ssh-connection = { path = "../ssh-connection" }
|
||||
ssh-transport = { path = "../ssh-transport" }
|
||||
tracing.workspace = true
|
||||
|
||||
5
lib/ssh-protocol/README.md
Normal file
5
lib/ssh-protocol/README.md
Normal file
|
|
@ -0,0 +1,5 @@
|
|||
# ssh-protocol
|
||||
|
||||
Combines `ssh-connection` and `ssh-transport` into a higher level interface.
|
||||
|
||||
Also implements authentication based on [RFC 4252 The Secure Shell (SSH) Authentication Protocol](https://datatracker.ietf.org/doc/html/rfc4252).
|
||||
420
lib/ssh-protocol/src/lib.rs
Normal file
420
lib/ssh-protocol/src/lib.rs
Normal file
|
|
@ -0,0 +1,420 @@
|
|||
use std::mem;
|
||||
|
||||
pub use ssh_connection as connection;
|
||||
use ssh_connection::ChannelOperation;
|
||||
pub use ssh_connection::{ChannelUpdate, ChannelUpdateKind};
|
||||
pub use ssh_transport as transport;
|
||||
pub use ssh_transport::{Result, SshStatus};
|
||||
use tracing::debug;
|
||||
|
||||
pub struct ServerConnection {
|
||||
transport: ssh_transport::server::ServerConnection,
|
||||
state: ServerConnectionState,
|
||||
}
|
||||
|
||||
enum ServerConnectionState {
|
||||
Auth(auth::BadAuth),
|
||||
Open(ssh_connection::ChannelsState),
|
||||
}
|
||||
|
||||
impl ServerConnection {
|
||||
pub fn new(transport: ssh_transport::server::ServerConnection) -> Self {
|
||||
Self {
|
||||
transport,
|
||||
state: ServerConnectionState::Auth(auth::BadAuth::new()),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn recv_bytes(&mut self, bytes: &[u8]) -> Result<()> {
|
||||
self.transport.recv_bytes(bytes)?;
|
||||
|
||||
while let Some(packet) = self.transport.next_plaintext_packet() {
|
||||
match &mut self.state {
|
||||
ServerConnectionState::Auth(auth) => {
|
||||
auth.recv_packet(packet)?;
|
||||
for to_send in auth.packets_to_send() {
|
||||
self.transport.send_plaintext_packet(to_send);
|
||||
}
|
||||
if auth.is_authenticated() {
|
||||
self.state =
|
||||
ServerConnectionState::Open(ssh_connection::ChannelsState::new(true));
|
||||
}
|
||||
}
|
||||
ServerConnectionState::Open(con) => {
|
||||
con.recv_packet(packet)?;
|
||||
}
|
||||
}
|
||||
|
||||
self.progress();
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub fn next_msg_to_send(&mut self) -> Option<ssh_transport::Msg> {
|
||||
self.transport.next_msg_to_send()
|
||||
}
|
||||
|
||||
pub fn next_channel_update(&mut self) -> Option<ssh_connection::ChannelUpdate> {
|
||||
match &mut self.state {
|
||||
ServerConnectionState::Auth(_) => None,
|
||||
ServerConnectionState::Open(con) => con.next_channel_update(),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn do_operation(&mut self, op: ChannelOperation) {
|
||||
match &mut self.state {
|
||||
ServerConnectionState::Auth(_) => panic!("tried to get connection during auth"),
|
||||
ServerConnectionState::Open(con) => {
|
||||
con.do_operation(op);
|
||||
self.progress();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub fn progress(&mut self) {
|
||||
match &mut self.state {
|
||||
ServerConnectionState::Auth(auth) => {
|
||||
for to_send in auth.packets_to_send() {
|
||||
self.transport.send_plaintext_packet(to_send);
|
||||
}
|
||||
}
|
||||
ServerConnectionState::Open(con) => {
|
||||
for to_send in con.packets_to_send() {
|
||||
self.transport.send_plaintext_packet(to_send);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub struct ClientConnection {
|
||||
transport: ssh_transport::client::ClientConnection,
|
||||
state: ClientConnectionState,
|
||||
}
|
||||
|
||||
enum ClientConnectionState {
|
||||
Setup(Option<auth::ClientAuth>),
|
||||
Auth(auth::ClientAuth),
|
||||
Open(ssh_connection::ChannelsState),
|
||||
}
|
||||
|
||||
impl ClientConnection {
|
||||
pub fn new(transport: ssh_transport::client::ClientConnection, auth: auth::ClientAuth) -> Self {
|
||||
Self {
|
||||
transport,
|
||||
state: ClientConnectionState::Setup(Some(auth)),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn recv_bytes(&mut self, bytes: &[u8]) -> Result<()> {
|
||||
self.transport.recv_bytes(bytes)?;
|
||||
|
||||
if let ClientConnectionState::Setup(auth) = &mut self.state {
|
||||
if let Some(session_ident) = self.transport.is_open() {
|
||||
let mut auth = mem::take(auth).unwrap();
|
||||
auth.set_session_identifier(session_ident);
|
||||
for to_send in auth.packets_to_send() {
|
||||
self.transport.send_plaintext_packet(to_send);
|
||||
}
|
||||
debug!("Connection has been opened");
|
||||
self.state = ClientConnectionState::Auth(auth);
|
||||
}
|
||||
}
|
||||
|
||||
while let Some(packet) = self.transport.next_plaintext_packet() {
|
||||
match &mut self.state {
|
||||
ClientConnectionState::Setup(_) => unreachable!("handled above"),
|
||||
ClientConnectionState::Auth(auth) => {
|
||||
auth.recv_packet(packet)?;
|
||||
for to_send in auth.packets_to_send() {
|
||||
self.transport.send_plaintext_packet(to_send);
|
||||
}
|
||||
if auth.is_authenticated() {
|
||||
self.state =
|
||||
ClientConnectionState::Open(ssh_connection::ChannelsState::new(false));
|
||||
}
|
||||
}
|
||||
ClientConnectionState::Open(con) => {
|
||||
con.recv_packet(packet)?;
|
||||
for to_send in con.packets_to_send() {
|
||||
self.transport.send_plaintext_packet(to_send);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub fn auth(&mut self) -> Option<&mut auth::ClientAuth> {
|
||||
match &mut self.state {
|
||||
ClientConnectionState::Auth(auth) => Some(auth),
|
||||
_ => None,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn is_open(&self) -> bool {
|
||||
matches!(self.state, ClientConnectionState::Open(_))
|
||||
}
|
||||
|
||||
pub fn next_msg_to_send(&mut self) -> Option<ssh_transport::Msg> {
|
||||
self.transport.next_msg_to_send()
|
||||
}
|
||||
|
||||
pub fn next_channel_update(&mut self) -> Option<ssh_connection::ChannelUpdate> {
|
||||
match &mut self.state {
|
||||
ClientConnectionState::Setup(_) => None,
|
||||
ClientConnectionState::Auth(_) => None,
|
||||
ClientConnectionState::Open(con) => con.next_channel_update(),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn do_operation(&mut self, op: ChannelOperation) {
|
||||
match &mut self.state {
|
||||
ClientConnectionState::Setup(_) | ClientConnectionState::Auth(_) => {
|
||||
panic!("tried to get connection during auth")
|
||||
}
|
||||
ClientConnectionState::Open(con) => {
|
||||
con.do_operation(op);
|
||||
self.progress();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub fn progress(&mut self) {
|
||||
match &mut self.state {
|
||||
ClientConnectionState::Setup(_) => {}
|
||||
ClientConnectionState::Auth(auth) => {
|
||||
for to_send in auth.packets_to_send() {
|
||||
self.transport.send_plaintext_packet(to_send);
|
||||
}
|
||||
}
|
||||
ClientConnectionState::Open(con) => {
|
||||
for to_send in con.packets_to_send() {
|
||||
self.transport.send_plaintext_packet(to_send);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// <https://datatracker.ietf.org/doc/html/rfc4252>
|
||||
pub mod auth {
|
||||
use std::collections::VecDeque;
|
||||
|
||||
use ssh_transport::{numbers, packet::Packet, parse::NameList, peer_error, Result};
|
||||
use tracing::info;
|
||||
|
||||
pub struct BadAuth {
|
||||
has_failed: bool,
|
||||
packets_to_send: VecDeque<Packet>,
|
||||
is_authenticated: bool,
|
||||
}
|
||||
|
||||
impl BadAuth {
|
||||
pub fn new() -> Self {
|
||||
Self {
|
||||
has_failed: false,
|
||||
packets_to_send: VecDeque::new(),
|
||||
is_authenticated: false,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn recv_packet(&mut self, packet: Packet) -> Result<()> {
|
||||
assert!(!self.is_authenticated, "Must not feed more packets to authentication after authentication is been completed, check with .is_authenticated()");
|
||||
|
||||
// This is a super simplistic implementation of RFC4252 SSH authentication.
|
||||
// We ask for a public key, and always let that one pass.
|
||||
// The reason for this is that this makes it a lot easier to test locally.
|
||||
// It's not very good, but it's good enough for now.
|
||||
let mut auth_req = packet.payload_parser();
|
||||
|
||||
if auth_req.u8()? != numbers::SSH_MSG_USERAUTH_REQUEST {
|
||||
return Err(peer_error!("did not send SSH_MSG_SERVICE_REQUEST"));
|
||||
}
|
||||
let username = auth_req.utf8_string()?;
|
||||
let service_name = auth_req.utf8_string()?;
|
||||
let method_name = auth_req.utf8_string()?;
|
||||
|
||||
if method_name != "none" {
|
||||
info!(
|
||||
%username,
|
||||
%service_name,
|
||||
%method_name,
|
||||
"User trying to authenticate"
|
||||
);
|
||||
}
|
||||
|
||||
if service_name != "ssh-connection" {
|
||||
return Err(peer_error!(
|
||||
"client tried to unsupported service: {service_name}"
|
||||
));
|
||||
}
|
||||
|
||||
match method_name {
|
||||
"password" => {
|
||||
let change_password = auth_req.bool()?;
|
||||
if change_password {
|
||||
return Err(peer_error!("client tried to change password unprompted"));
|
||||
}
|
||||
let password = auth_req.utf8_string()?;
|
||||
|
||||
info!(%password, "Got password");
|
||||
// Don't worry queen, your password is correct!
|
||||
self.queue_packet(Packet::new_msg_userauth_success());
|
||||
self.is_authenticated = true;
|
||||
}
|
||||
"publickey" => {
|
||||
info!("Got public key");
|
||||
// Don't worry queen, your key is correct!
|
||||
self.queue_packet(Packet::new_msg_userauth_success());
|
||||
self.is_authenticated = true;
|
||||
}
|
||||
_ if self.has_failed => {
|
||||
return Err(peer_error!(
|
||||
"client tried unsupported method twice: {method_name}"
|
||||
));
|
||||
}
|
||||
_ => {
|
||||
// Initial.
|
||||
|
||||
self.queue_packet(Packet::new_msg_userauth_banner(
|
||||
b"!! this system ONLY allows catgirls to enter !!\r\n\
|
||||
!! all other attempts WILL be prosecuted to the full extent of the rawr !!\r\n\
|
||||
!! THIS SYTEM WILL LOG AND STORE YOUR CLEARTEXT PASSWORD !!\r\n\
|
||||
!! DO NOT ENTER PASSWORDS YOU DON'T WANT STOLEN !!\r\n",
|
||||
b"",
|
||||
));
|
||||
|
||||
self.queue_packet(Packet::new_msg_userauth_failure(
|
||||
NameList::one("password"),
|
||||
false,
|
||||
));
|
||||
// Stay in the same state
|
||||
}
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub fn packets_to_send(&mut self) -> impl Iterator<Item = Packet> + '_ {
|
||||
self.packets_to_send.drain(..)
|
||||
}
|
||||
|
||||
pub fn is_authenticated(&self) -> bool {
|
||||
self.is_authenticated
|
||||
}
|
||||
|
||||
fn queue_packet(&mut self, packet: Packet) {
|
||||
self.packets_to_send.push_back(packet);
|
||||
}
|
||||
}
|
||||
|
||||
pub struct ClientAuth {
|
||||
username: Vec<u8>,
|
||||
packets_to_send: VecDeque<Packet>,
|
||||
user_requests: VecDeque<ClientUserRequest>,
|
||||
is_authenticated: bool,
|
||||
session_identifier: Option<[u8; 32]>,
|
||||
}
|
||||
|
||||
pub enum ClientUserRequest {
|
||||
Password,
|
||||
PrivateKeySign { session_identifier: [u8; 32] },
|
||||
Banner(Vec<u8>),
|
||||
}
|
||||
|
||||
impl ClientAuth {
|
||||
pub fn new(username: Vec<u8>) -> Self {
|
||||
let mut packets_to_send = VecDeque::new();
|
||||
let initial_useruath_req =
|
||||
Packet::new_msg_userauth_request_none(&username, b"ssh-connection", b"none");
|
||||
packets_to_send.push_back(initial_useruath_req);
|
||||
|
||||
Self {
|
||||
packets_to_send,
|
||||
username,
|
||||
user_requests: VecDeque::new(),
|
||||
is_authenticated: false,
|
||||
session_identifier: None,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn set_session_identifier(&mut self, ident: [u8; 32]) {
|
||||
assert!(self.session_identifier.is_none());
|
||||
self.session_identifier = Some(ident);
|
||||
}
|
||||
|
||||
pub fn is_authenticated(&self) -> bool {
|
||||
self.is_authenticated
|
||||
}
|
||||
|
||||
pub fn packets_to_send(&mut self) -> impl Iterator<Item = Packet> + '_ {
|
||||
self.packets_to_send.drain(..)
|
||||
}
|
||||
|
||||
pub fn user_requests(&mut self) -> impl Iterator<Item = ClientUserRequest> + '_ {
|
||||
self.user_requests.drain(..)
|
||||
}
|
||||
|
||||
pub fn send_password(&mut self, password: &str) {
|
||||
let packet = Packet::new_msg_userauth_request_password(
|
||||
&self.username,
|
||||
b"ssh-connection",
|
||||
b"password",
|
||||
false,
|
||||
password.as_bytes(),
|
||||
);
|
||||
self.packets_to_send.push_back(packet);
|
||||
}
|
||||
|
||||
pub fn recv_packet(&mut self, packet: Packet) -> Result<()> {
|
||||
assert!(!self.is_authenticated, "Must not feed more packets to authentication after authentication is been completed, check with .is_authenticated()");
|
||||
|
||||
let mut p = packet.payload_parser();
|
||||
let packet_type = p.u8()?;
|
||||
|
||||
match packet_type {
|
||||
numbers::SSH_MSG_USERAUTH_BANNER => {
|
||||
let banner = p.string()?;
|
||||
let _lang = p.string()?;
|
||||
|
||||
self.user_requests
|
||||
.push_back(ClientUserRequest::Banner(banner.to_vec()));
|
||||
}
|
||||
numbers::SSH_MSG_USERAUTH_FAILURE => {
|
||||
let authentications = p.name_list()?;
|
||||
let _partial_success = p.bool()?;
|
||||
|
||||
if authentications.iter().any(|item| item == "password") {
|
||||
self.user_requests.push_back(ClientUserRequest::Password);
|
||||
} else if authentications.iter().any(|item| item == "publickey") {
|
||||
// <https://datatracker.ietf.org/doc/html/rfc4252#section-7>
|
||||
// TODO: Ask the server whether there are any keys we can use instead of just yoloing the signature.
|
||||
self.user_requests
|
||||
.push_back(ClientUserRequest::PrivateKeySign {
|
||||
session_identifier: self
|
||||
.session_identifier
|
||||
.expect("set_session_identifier has not been called"),
|
||||
});
|
||||
} else {
|
||||
return Err(peer_error!(
|
||||
"server does not support password authentication"
|
||||
));
|
||||
}
|
||||
}
|
||||
numbers::SSH_MSG_USERAUTH_SUCCESS => {
|
||||
self.is_authenticated = true;
|
||||
}
|
||||
_ => {
|
||||
return Err(peer_error!(
|
||||
"unexpected packet: {}",
|
||||
numbers::packet_type_to_string(packet_type)
|
||||
))
|
||||
}
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue