From b9ae35b1cb639a0e1d80eec9ea8c56a36e9664f5 Mon Sep 17 00:00:00 2001 From: Lucas Chollet Date: Mon, 23 Mar 2026 15:03:47 +0100 Subject: [PATCH 1/2] SSHServer: Add a naive implementation of the userauth service This implementation allows any authentication from the current user with the `none` method. It is a MAJOR SECURITY HOLE, use at your own risk. Other authentication methods should be added soon to solve this issue. --- Userland/Services/SSHServer/SSHClient.cpp | 47 ++++++++++++++++++++++- Userland/Services/SSHServer/SSHClient.h | 4 ++ Userland/Services/SSHServer/main.cpp | 5 ++- 3 files changed, 53 insertions(+), 3 deletions(-) diff --git a/Userland/Services/SSHServer/SSHClient.cpp b/Userland/Services/SSHServer/SSHClient.cpp index da59d1d4cdf3de..8dcf4c0c8a0ea8 100644 --- a/Userland/Services/SSHServer/SSHClient.cpp +++ b/Userland/Services/SSHServer/SSHClient.cpp @@ -11,6 +11,7 @@ #include #include #include +#include #include #include #include @@ -36,6 +37,8 @@ ErrorOr SSHClient::handle_data(ByteBuffer& data) return {}; case State::KeyExchanged: return handle_service_request(TRY(unpack_generic_message(data))); + case State::WaitingForUserAuthentication: + return handle_user_authentication(TRY(unpack_generic_message(data))); case State::Authentified: return Error::from_string_literal("Draw the rest of the owl"); } @@ -262,7 +265,7 @@ ErrorOr SSHClient::handle_service_request(GenericMessage message) // " TRY(send_service_accept(service_name)); - m_state = State::Authentified; + m_state = State::WaitingForUserAuthentication; return {}; } @@ -281,4 +284,46 @@ ErrorOr SSHClient::send_service_accept(StringView service_name) return {}; } +// 5. Authentication Requests +// https://datatracker.ietf.org/doc/html/rfc4252#section-5 +ErrorOr SSHClient::handle_user_authentication(GenericMessage message) +{ + if (message.type != MessageID::USERAUTH_REQUEST) + return Error::from_string_literal("Expected packet of type USERAUTH_REQUEST"); + + auto username = TRY(decode_string(message.payload)); + auto service_name = TRY(decode_string(message.payload)); + auto method_name = TRY(decode_string(message.payload)); + + dbgln_if(SSH_DEBUG, "User authentication username: {:s}", username.bytes()); + dbgln_if(SSH_DEBUG, "User authentication service_name: {:s}", service_name.bytes()); + dbgln_if(SSH_DEBUG, "User authentication method name: {:s}", method_name.bytes()); + + if (username != TRY(Core::Account::self(Core::Account::Read::PasswdOnly)).username()) + return Error::from_string_literal("Can't authenticate for another user account"); + + if (method_name == "none"sv.bytes()) { + // FIXME: Implement proper authentication!!! + + m_state = State::Authentified; + TRY(send_user_authentication_success()); + + // FIXME: Also send a cool banner :^) + + return {}; + } + + return Error::from_string_literal("Unsupported userauth method"); +} + +// 5.1. Responses to Authentication Requests +// https://datatracker.ietf.org/doc/html/rfc4252#section-5.1 +ErrorOr SSHClient::send_user_authentication_success() +{ + AllocatingMemoryStream stream; + TRY(stream.write_value(MessageID::USERAUTH_SUCCESS)); + TRY(write_packet(TRY(stream.read_until_eof()))); + return {}; +} + } // SSHServer diff --git a/Userland/Services/SSHServer/SSHClient.h b/Userland/Services/SSHServer/SSHClient.h index 70ab4d2cb1b0aa..a4c9acac36eb24 100644 --- a/Userland/Services/SSHServer/SSHClient.h +++ b/Userland/Services/SSHServer/SSHClient.h @@ -37,6 +37,7 @@ class SSHClient : public Peer { WaitingForKeyExchange, WaitingForNewKeysMessage, KeyExchanged, + WaitingForUserAuthentication, Authentified, }; @@ -53,6 +54,9 @@ class SSHClient : public Peer { ErrorOr handle_service_request(GenericMessage data); ErrorOr send_service_accept(StringView); + ErrorOr handle_user_authentication(GenericMessage data); + ErrorOr send_user_authentication_success(); + State m_state { State::Constructed }; Core::TCPSocket& m_tcp_socket; diff --git a/Userland/Services/SSHServer/main.cpp b/Userland/Services/SSHServer/main.cpp index 5da20ee799cf2f..22476e2bad67e7 100644 --- a/Userland/Services/SSHServer/main.cpp +++ b/Userland/Services/SSHServer/main.cpp @@ -18,7 +18,8 @@ static constexpr auto DEFAULT_PORT = 22; ErrorOr serenity_main(Main::Arguments args) { - TRY(Core::System::pledge("stdio accept inet unix")); + TRY(Core::System::pledge("stdio accept inet unix rpath")); + TRY(Core::System::unveil("/etc/passwd", "r")); TRY(Core::System::unveil(nullptr, nullptr)); Optional port {}; @@ -54,6 +55,6 @@ ErrorOr serenity_main(Main::Arguments args) outln("Listening on {}:{}", tcp_server->local_address().value(), tcp_server->local_port()); - TRY(Core::System::pledge("stdio accept")); + TRY(Core::System::pledge("stdio accept rpath")); return loop.exec(); } From 4878bac2257bf98e18635cba98e57226516bdfec Mon Sep 17 00:00:00 2001 From: Lucas Chollet Date: Mon, 23 Mar 2026 16:44:15 +0100 Subject: [PATCH 2/2] LibSSH+SSHServer: Support basic channels and sessions This allows a client to execute commands on the server. This is very minimal and don't support interactive sessions yet. It is however enough to run simple commands like `uname` over the network :^) --- Userland/Libraries/LibSSH/CMakeLists.txt | 1 + Userland/Libraries/LibSSH/Session.cpp | 21 +++ Userland/Libraries/LibSSH/Session.h | 25 ++++ Userland/Services/SSHServer/SSHClient.cpp | 163 +++++++++++++++++++++- Userland/Services/SSHServer/SSHClient.h | 13 ++ Userland/Services/SSHServer/main.cpp | 5 +- 6 files changed, 224 insertions(+), 4 deletions(-) create mode 100644 Userland/Libraries/LibSSH/Session.cpp create mode 100644 Userland/Libraries/LibSSH/Session.h diff --git a/Userland/Libraries/LibSSH/CMakeLists.txt b/Userland/Libraries/LibSSH/CMakeLists.txt index 933562fe1355c8..5ee2fdd3bfaf23 100644 --- a/Userland/Libraries/LibSSH/CMakeLists.txt +++ b/Userland/Libraries/LibSSH/CMakeLists.txt @@ -4,6 +4,7 @@ set(SOURCES IdentificationString.cpp KeyExchangeData.cpp Peer.cpp + Session.cpp ) serenity_lib(LibSSH ssh) diff --git a/Userland/Libraries/LibSSH/Session.cpp b/Userland/Libraries/LibSSH/Session.cpp new file mode 100644 index 00000000000000..ec74650e88e0b4 --- /dev/null +++ b/Userland/Libraries/LibSSH/Session.cpp @@ -0,0 +1,21 @@ +/* + * Copyright (c) 2026, Lucas Chollet + * + * SPDX-License-Identifier: BSD-2-Clause + */ + +#include "Session.h" + +namespace SSH { + +ErrorOr Session::create(u32 sender_channel_id, u32 window_size, u32 maximum_packet_size) +{ + Session session; + session.local_channel_id = sender_channel_id; + session.sender_channel_id = sender_channel_id; + session.maximum_packet_size = maximum_packet_size; + session.window = TRY(ByteBuffer::create_zeroed(window_size)); + return session; +} + +} diff --git a/Userland/Libraries/LibSSH/Session.h b/Userland/Libraries/LibSSH/Session.h new file mode 100644 index 00000000000000..35daa2bbe5e4be --- /dev/null +++ b/Userland/Libraries/LibSSH/Session.h @@ -0,0 +1,25 @@ +/* + * Copyright (c) 2026, Lucas Chollet + * + * SPDX-License-Identifier: BSD-2-Clause + */ + +#pragma once + +#include +#include + +namespace SSH { + +// 6.1. Opening a Session +// https://datatracker.ietf.org/doc/html/rfc4254#section-6.1 +struct Session { + static ErrorOr create(u32 sender_channel_id, u32 window_size, u32 maximum_packet_size); + + u32 local_channel_id {}; + u32 sender_channel_id {}; + u32 maximum_packet_size {}; + ByteBuffer window {}; +}; + +} diff --git a/Userland/Services/SSHServer/SSHClient.cpp b/Userland/Services/SSHServer/SSHClient.cpp index 8dcf4c0c8a0ea8..29cc6308b47fcc 100644 --- a/Userland/Services/SSHServer/SSHClient.cpp +++ b/Userland/Services/SSHServer/SSHClient.cpp @@ -12,6 +12,7 @@ #include #include #include +#include #include #include #include @@ -23,7 +24,6 @@ namespace SSH::Server { ErrorOr SSHClient::handle_data(ByteBuffer& data) { - switch (m_state) { case State::Constructed: return handle_protocol_version(data); @@ -40,7 +40,8 @@ ErrorOr SSHClient::handle_data(ByteBuffer& data) case State::WaitingForUserAuthentication: return handle_user_authentication(TRY(unpack_generic_message(data))); case State::Authentified: - return Error::from_string_literal("Draw the rest of the owl"); + TRY(handle_generic_packet(TRY(unpack_generic_message(data)))); + return {}; } VERIFY_NOT_REACHED(); } @@ -326,4 +327,162 @@ ErrorOr SSHClient::send_user_authentication_success() return {}; } +ErrorOr SSHClient::handle_generic_packet(GenericMessage&& message) +{ + switch (message.type) { + case MessageID::CHANNEL_OPEN: + return handle_channel_open_message(message); + case MessageID::CHANNEL_REQUEST: + return handle_channel_request(message); + default: + dbgln_if(SSH_DEBUG, "Unexpected packet: {}", to_underlying(message.type)); + return Error::from_string_literal("Unexpected packet type"); + } + VERIFY_NOT_REACHED(); +} + +// 5.1. Opening a Channel +// https://datatracker.ietf.org/doc/html/rfc4254#section-5.1 +ErrorOr SSHClient::handle_channel_open_message(GenericMessage& message) +{ + auto channel_type = TRY(decode_string(message.payload)); + u32 sender_channel_id = TRY(message.payload.read_value>()); + u32 initial_window_size = TRY(message.payload.read_value>()); + u32 maximum_packet_size = TRY(message.payload.read_value>()); + + dbgln_if(SSH_DEBUG, "Channel open request with: {:s} - {} - {} - {}", + channel_type.bytes(), sender_channel_id, initial_window_size, maximum_packet_size); + + if (channel_type != "session"sv.bytes()) + return Error::from_string_literal("Unexpected channel type"); + + m_sessions.empend(TRY(Session::create(sender_channel_id, initial_window_size, maximum_packet_size))); + + TRY(send_channel_open_confirmation(m_sessions.last())); + + return {}; +} + +// 5.1. Opening a Channel +// https://datatracker.ietf.org/doc/html/rfc4254#section-5.1 +ErrorOr SSHClient::send_channel_open_confirmation(Session const& session) +{ + AllocatingMemoryStream stream; + + // byte SSH_MSG_CHANNEL_OPEN_CONFIRMATION + // uint32 recipient channel + // uint32 sender channel + // uint32 initial window size + // uint32 maximum packet size + + TRY(stream.write_value(MessageID::CHANNEL_OPEN_CONFIRMATION)); + // "The 'recipient channel' is the channel number given in the original + // open request, and 'sender channel' is the channel number allocated by + // the other side." + TRY(stream.write_value>(session.sender_channel_id)); + TRY(stream.write_value>(session.local_channel_id)); + + TRY(stream.write_value>(session.window.size())); + TRY(stream.write_value>(session.maximum_packet_size)); + + TRY(write_packet(TRY(stream.read_until_eof()))); + return {}; +} + +ErrorOr SSHClient::find_session(u32 sender_channel_id) +{ + for (auto& session : m_sessions) { + if (session.sender_channel_id == sender_channel_id) + return &session; + } + return Error::from_string_literal("Session not found"); +} + +// 5.4. Channel-Specific Requests +// https://datatracker.ietf.org/doc/html/rfc4254#section-5.4 +ErrorOr SSHClient::handle_channel_request(GenericMessage& message) +{ + auto recipient_channel_id = TRY(message.payload.read_value>()); + auto request_type = TRY(decode_string(message.payload)); + auto want_reply = TRY(message.payload.read_value()); + + auto& session = *TRY(find_session(recipient_channel_id)); + + dbgln_if(SSH_DEBUG, "CHANNEL_REQUEST id({}): {:s}", session.local_channel_id, request_type.bytes()); + + if (request_type == "env"sv.bytes() && !want_reply) { + dbgln("FIXME: Ignored channel request: {:s}", request_type.bytes()); + return {}; + } + + if (request_type == "exec"sv.bytes()) { + auto command = TRY(decode_string(message.payload)); + + // FIXME: This is a naive implementation, we should stream the result back + // to the user and not block the event loop during the execution of + // the command. + // We should also use the user's shell rather than hardcoding it. + +#ifdef AK_OS_SERENITY + auto shell = "/bin/Shell"sv; +#else + auto shell = "/bin/sh"sv; +#endif + + Vector args; + args.append(shell); + args.append("-c"); + args.append(ByteString(command.bytes())); + + Vector raw_args; + raw_args.ensure_capacity(args.size() + 1); + for (auto& arg : args) + raw_args.append(arg.characters()); + + raw_args.append(nullptr); + + auto child = TRY(Core::Command::create(shell, raw_args.data())); + auto output = TRY(child->read_all()); + auto status = TRY(child->status()); + + if (status != Core::Command::ProcessResult::DoneWithZeroExitCode) + return Error::from_string_literal("Unable to run command"); + + TRY(send_channel_success_message(session)); + TRY(send_channel_data(session, output.standard_output)); + TRY(send_channel_close(session)); + return {}; + } + + return Error::from_string_literal("Unsupported channel request"); +} + +ErrorOr SSHClient::send_channel_success_message(Session const& session) +{ + AllocatingMemoryStream stream; + TRY(stream.write_value(MessageID::CHANNEL_SUCCESS)); + TRY(stream.write_value>(session.local_channel_id)); + TRY(write_packet(TRY(stream.read_until_eof()))); + return {}; +} + +ErrorOr SSHClient::send_channel_data(Session const& session, ByteBuffer const& data) +{ + AllocatingMemoryStream stream; + TRY(stream.write_value(MessageID::CHANNEL_DATA)); + TRY(stream.write_value>(session.local_channel_id)); + TRY(encode_string(stream, data)); + TRY(write_packet(TRY(stream.read_until_eof()))); + return {}; +} + +ErrorOr SSHClient::send_channel_close(Session const& session) +{ + AllocatingMemoryStream stream; + TRY(stream.write_value(MessageID::CHANNEL_CLOSE)); + TRY(stream.write_value>(session.local_channel_id)); + TRY(write_packet(TRY(stream.read_until_eof()))); + return {}; +} + } // SSHServer diff --git a/Userland/Services/SSHServer/SSHClient.h b/Userland/Services/SSHServer/SSHClient.h index a4c9acac36eb24..321664a62ba1ba 100644 --- a/Userland/Services/SSHServer/SSHClient.h +++ b/Userland/Services/SSHServer/SSHClient.h @@ -10,6 +10,7 @@ #include #include #include +#include namespace SSH::Server { @@ -57,10 +58,22 @@ class SSHClient : public Peer { ErrorOr handle_user_authentication(GenericMessage data); ErrorOr send_user_authentication_success(); + ErrorOr handle_generic_packet(GenericMessage&&); + + ErrorOr handle_channel_open_message(GenericMessage&); + ErrorOr send_channel_open_confirmation(Session const&); + ErrorOr handle_channel_request(GenericMessage&); + ErrorOr send_channel_success_message(Session const&); + ErrorOr send_channel_data(Session const&, ByteBuffer const&); + ErrorOr send_channel_close(Session const&); + ErrorOr find_session(u32 sender_channel_id); + State m_state { State::Constructed }; Core::TCPSocket& m_tcp_socket; KeyExchangeData m_key_exchange_data {}; + + Vector m_sessions; }; } // SSHServer diff --git a/Userland/Services/SSHServer/main.cpp b/Userland/Services/SSHServer/main.cpp index 22476e2bad67e7..fdf5108c8b48dd 100644 --- a/Userland/Services/SSHServer/main.cpp +++ b/Userland/Services/SSHServer/main.cpp @@ -18,8 +18,9 @@ static constexpr auto DEFAULT_PORT = 22; ErrorOr serenity_main(Main::Arguments args) { - TRY(Core::System::pledge("stdio accept inet unix rpath")); + TRY(Core::System::pledge("stdio accept inet unix rpath proc exec")); TRY(Core::System::unveil("/etc/passwd", "r")); + TRY(Core::System::unveil("/bin/Shell", "rx")); TRY(Core::System::unveil(nullptr, nullptr)); Optional port {}; @@ -55,6 +56,6 @@ ErrorOr serenity_main(Main::Arguments args) outln("Listening on {}:{}", tcp_server->local_address().value(), tcp_server->local_port()); - TRY(Core::System::pledge("stdio accept rpath")); + TRY(Core::System::pledge("stdio accept rpath proc exec")); return loop.exec(); }