Move files around, add UI

This commit is contained in:
Michael Mikovsky
2025-06-05 16:02:28 -06:00
parent 8adfc68854
commit 92c9f08a5c
35 changed files with 541 additions and 132 deletions
+9 -2
View File
@@ -1,10 +1,17 @@
[package] [package]
name = "unshell-rs" name = "server"
version = "0.1.0" version = "0.1.0"
edition = "2024" edition = "2024"
[dependencies] [dependencies]
base64 = "0.22.1" clap = { version = "4.5.39", features = ["derive"] }
log = "0.4.27" log = "0.4.27"
pretty_env_logger = "0.5.0"
serde = { version = "1.0.219", features = ["derive"] } serde = { version = "1.0.219", features = ["derive"] }
serde_json = "1.0.140" serde_json = "1.0.140"
slint = "1.11.0"
unshell-rs-lib = { path = "./unshell-rs-lib" }
[build-dependencies]
slint-build = "1.11.0"
+1
View File
@@ -6,6 +6,7 @@
- Probably out of scope - Probably out of scope
- Build targets - Build targets
- To achieve a minimal size, there should probably be a way to pack diffrent features with the actual result binary. - To achieve a minimal size, there should probably be a way to pack diffrent features with the actual result binary.
- There should also be a way to update one of the hosts with the new functionality.
### Network ### Network
- Diffrent traffic obfuscators: - Diffrent traffic obfuscators:
+3
View File
@@ -0,0 +1,3 @@
fn main() {
slint_build::compile("ui/app-window.slint").expect("Slint build failed");
}
+2 -1
View File
@@ -4,5 +4,6 @@ version = "0.1.0"
edition = "2024" edition = "2024"
[dependencies] [dependencies]
serde_json = "1.0.140"
# libc = "0.2.172" # libc = "0.2.172"
unshell-rs = { path = "../" } unshell-rs-lib = { path = "../unshell-rs-lib" }
+55 -11
View File
@@ -2,20 +2,64 @@
// mod execute; // mod execute;
use std::error::Error; use std::error::Error;
use unshell_rs::{
networkers::{TCPClient, TCPConnection}, use std::{
payload::run_client, sync::{Arc, Mutex},
thread,
time::Duration,
}; };
// /// Pipe streams are blocking, we need separate threads to monitor them without blocking the primary thread. use unshell_rs_lib::{
// fn child_stream_to_vec<R>(mut stream: R) -> Arc<Mutex<Vec<u8>>> networkers::{ClientTrait, Connection, TCPClient, TCPConnection},
// where packets::Packet,
// R: Read + Send + 'static, };
// {
// let out = Arc::new(Mutex::new(Vec::new()));
// let vec = out.clone();
// } // Generic client function
pub fn run_client<C, Cl>(address: &str) -> Result<(), Box<dyn std::error::Error>>
where
Cl: ClientTrait<C>,
C: Connection + 'static,
Cl::Error: std::error::Error + 'static,
C::Error: std::error::Error + 'static,
{
let recv_conn = Arc::new(Mutex::new(Cl::connect(address)?));
let transmit_vec: Arc<Mutex<Vec<Packet>>> = Arc::new(Mutex::new(Vec::new()));
let transmit_conn = Arc::clone(&recv_conn);
let transmit_vec_clone = Arc::clone(&transmit_vec);
thread::spawn(move || {
loop {
let mut transmit_vec_lock = transmit_vec.lock().unwrap();
if transmit_vec_lock.len() > 0 {
let mut conn_lock = recv_conn.lock().unwrap();
if let Ok(json) = serde_json::to_string(&transmit_vec_lock.pop().unwrap()) {
conn_lock.write(&json).expect("Failed to send packet!");
}
} else {
thread::sleep(Duration::from_millis(10));
}
}
});
loop {
let mut conn_lock = transmit_conn.lock().unwrap();
let data = conn_lock.read();
drop(conn_lock);
match data {
Ok(data_json) => {
if data_json.is_empty() {
continue;
}
let packet = serde_json::from_str::<Packet>(data_json.as_str());
println!("{:?}", packet);
}
Err(e) => {
eprintln!("Error reading, {}", e);
}
}
}
}
fn main() -> Result<(), Box<dyn Error>> { fn main() -> Result<(), Box<dyn Error>> {
run_client::<TCPConnection, TCPClient>("127.0.0.1:3000")?; run_client::<TCPConnection, TCPClient>("127.0.0.1:3000")?;
-7
View File
@@ -1,7 +0,0 @@
[package]
name = "server"
version = "0.1.0"
edition = "2024"
[dependencies]
unshell-rs = { path = "../" }
-14
View File
@@ -1,14 +0,0 @@
use std::error::Error;
use unshell_rs::{
listeners::Listener,
networkers::{ServerTrait, TCPServer},
};
fn main() -> Result<(), Box<dyn Error>> {
let mut server = Listener::new(TCPServer::bind("0.0.0.0:3000")?);
server.run_listener()?;
Ok(())
}
+62
View File
@@ -0,0 +1,62 @@
use slint::{ModelRc, VecModel};
use std::error::Error;
use unshell_rs_lib::config::listeners::ListenerConfig;
pub struct Unshell_Gui;
slint::include_modules!();
impl Unshell_Gui {
pub fn start() -> Result<Self, Box<dyn Error>> {
let ui = AppWindow::new()?;
// ui.
let ui_handle = ui.as_weak();
ui.on_tab_clicked(move |index| {
let ui = ui_handle.unwrap();
ui.set_current_tab(index);
trace!("Tab {} selected", index);
});
ui.set_app_info({
(String::new()
+ "Unshell\n"
+ "Version "
+ env!("CARGO_PKG_VERSION")
+ "\n\n View the source code at:\n https://github.com/astatin3/unshell-rs")
.into()
});
let listeners: Vec<ListenerConfig> = vec![ListenerConfig::Tcp {
enabled: true,
name: "test".to_string(),
remote_host: "127.0.0.1".to_string(),
port: 25565,
layers: Vec::new(),
}];
ui.set_listeners(ModelRc::new(VecModel::from(
listeners
.iter()
.map(|l| match l {
ListenerConfig::Tcp {
enabled,
name,
remote_host,
port,
layers,
} => UITcpListener {
enabled: *enabled,
name: name.clone().into(),
remote_host: remote_host.clone().into(),
port: *port as i32,
},
})
.collect::<Vec<UITcpListener>>(),
)));
ui.run()?;
Ok(Self {})
}
}
+5 -5
View File
@@ -1,5 +1,5 @@
pub mod layers; #[macro_use]
pub mod listeners; extern crate log;
pub mod networkers;
pub mod packets; mod gui;
pub mod payload; mod server;
+73
View File
@@ -0,0 +1,73 @@
use std::error::Error;
use clap::{Parser, Subcommand};
use log::trace;
use slint::{ModelRc, VecModel};
use unshell_rs_lib::{
config::listeners::ListenerConfig,
listeners::Listener,
networkers::{ServerTrait, TCPServer},
};
/// The default port that this program looks for
pub static DEFAULT_SERVICE_PORT: u16 = 13370;
/// The default website port that this program looks for
pub static DEFAULT_WEB_PORT: u16 = 8082;
#[derive(Debug, Parser)]
#[command(name = "unshell-rs")]
#[command(about = "Slick reverse shell tool in rust", long_about = None)]
struct Args {
#[command(subcommand)]
command: Commands,
}
#[derive(Debug, Subcommand)]
enum Commands {
/// Run as a service, and potentially hosting a website
#[command(arg_required_else_help = true)]
Serve {
/// Only listen for command clients locally
#[arg(short, long, default_value_t = false)]
local: bool,
/// Port listen to for command clients
#[arg(short, long, default_value_t = DEFAULT_SERVICE_PORT)]
service_port: u16,
// /// Port to listen for website traffic (0 is disabled)
// #[arg(short, long, default_value_t = DEFAULT_SERVICE_PORT)]
// web_port: u16,
},
Gui {
/// Listen for command clients remotely aswell
#[arg(short, long, default_value_t = true)]
remote: bool,
/// Port listen to for command clients
#[arg(short, long, default_value_t = DEFAULT_SERVICE_PORT)]
service_port: u16,
},
}
fn main() -> Result<(), Box<dyn Error>> {
pretty_env_logger::init();
let args = Args::parse();
match args.command {
Commands::Gui {
remote,
service_port,
} => {}
Commands::Serve {
local,
service_port,
// web_port,
} => {}
}
// let mut server = Listener::new(TCPServer::bind("0.0.0.0:3000")?);
// server.run_listener()?;
Ok(())
}
-80
View File
@@ -1,80 +0,0 @@
use std::{
sync::{Arc, Mutex},
thread,
time::Duration,
};
use crate::{
networkers::{ClientTrait, Connection},
packets::Packet,
};
// Generic client function
pub fn run_client<C, Cl>(address: &str) -> Result<(), Box<dyn std::error::Error>>
where
Cl: ClientTrait<C>,
C: Connection + 'static,
Cl::Error: std::error::Error + 'static,
C::Error: std::error::Error + 'static,
{
let recv_conn = Arc::new(Mutex::new(Cl::connect(address)?));
let transmit_vec: Arc<Mutex<Vec<Packet>>> = Arc::new(Mutex::new(Vec::new()));
let transmit_conn = Arc::clone(&recv_conn);
let transmit_vec_clone = Arc::clone(&transmit_vec);
thread::spawn(move || {
loop {
let mut transmit_vec_lock = transmit_vec.lock().unwrap();
if transmit_vec_lock.len() > 0 {
let mut conn_lock = recv_conn.lock().unwrap();
if let Ok(json) = serde_json::to_string(&transmit_vec_lock.pop().unwrap()) {
conn_lock.write(&json).expect("Failed to send packet!");
}
} else {
thread::sleep(Duration::from_millis(10));
}
}
});
loop {
let mut conn_lock = transmit_conn.lock().unwrap();
let data = conn_lock.read();
drop(conn_lock);
match data {
Ok(data_json) => {
if data_json.is_empty() {
continue;
}
let packet = serde_json::from_str::<Packet>(data_json.as_str());
println!("{:?}", packet);
}
Err(e) => {
eprintln!("Error reading, {}", e);
}
}
}
// loop {
// let mut input = String::new();
// stdin.read_line(&mut input)?;
// let input = input.trim();
// if input == "quit" {
// conn.write(input)?;
// break;
// }
// if !input.is_empty() {
// conn.write(input)?;
// match conn.read() {
// Ok(response) => println!("Server: {}", response),
// Err(e) => {
// eprintln!("Failed to read response: {:?}", e);
// break;
// }
// }
// }
// }
}
View File
+161
View File
@@ -0,0 +1,161 @@
import { Button, HorizontalBox, VerticalBox, Slider, StandardButton } from "std-widgets.slint";
import { DashboardPage } from "pages/dashboard.slint";
import { ListenersPage } from "pages/listeners.slint";
import { ClientsPage } from "pages/clients.slint";
import { ToolsPage } from "pages/tools.slint";
import { UITcpListener } from "structs.slint";
import { BorderedRectangle } from "components.slint";
component SideButton inherits Button {
in property <length> sidebar_size: 40px;
preferred-width: sidebar_size;
height: sidebar_size;
}
export component AppWindow inherits Window {
in-out property <int> current-tab: 0;
in-out property <[UITcpListener]> listeners;
in-out property <string> app-info;
callback tab-clicked(int);
tab-clicked(index) => {
current-tab = index;
}
// background: @linear-gradient(20deg, #1a161d 0%, #27222a 100%);
MenuBar {
Menu {
title: @tr("File");
MenuItem {
title: @tr("New");
activated => {
// file-new();
}
}
MenuItem {
title: @tr("Open");
activated => {
// file-open();
}
}
}
Menu {
title: @tr("Edit");
MenuItem {
title: @tr("Copy");
}
MenuItem {
title: @tr("Paste");
}
MenuSeparator { }
Menu {
title: @tr("Find");
MenuItem {
title: @tr("Find in document...");
}
MenuItem {
title: @tr("Find Next");
}
MenuItem {
title: @tr("Find Previous");
}
}
}
}
callback request-increase-value();
HorizontalLayout {
BorderedRectangle {
VerticalLayout {
spacing: 5px;
padding: 10px;
Text {
text: "Unshell";
font-size: 14px;
font-weight: 1000;
horizontal-alignment: center;
}
for entry[i] in [
{ name: "Dashboard" },
{ name: "Listeners" },
{ name: "Clients" },
{ name: "Tools" },
]: SideButton {
text: entry.name;
clicked => {
root.tab-clicked(i);
}
}
// Strechy
Rectangle { }
SideButton {
text: "Info";
clicked => {
info-window.visible = true;
}
}
}
}
// Text {
// text: "Counter: \{root.counter}";
// }
Rectangle {
if current-tab == 0: DashboardPage { }
if current-tab == 1: ListenersPage {
listeners: listeners;
}
if current-tab == 2: ClientsPage { }
if current-tab == 3: ToolsPage { }
}
callback file-new();
callback file-open();
}
info-window := Dialog {
visible: false;
BorderedRectangle {
VerticalLayout {
padding: 10px;
spacing: 10px;
Text {
font-size: 20px;
text: app-info;
}
StandardButton {
kind: ok;
clicked => {
info-window.visible = false;
}
}
}
}
}
}
+27
View File
@@ -0,0 +1,27 @@
export component BorderedRectangle inherits Rectangle {
background: #2a232a;
border-width: 1px;
border-color: darkslateblue;
}
export component BoolText inherits Text {
in property <bool> state;
text: state ? "TRUE" : "FALSE";
color: state ? #00ff00 : #ff0000;
}
export component TitleText inherits HorizontalLayout {
in property <string> text;
alignment: start;
Text {
text: text;
font-weight: 700;
font-size: 15px;
}
@children
}
+5
View File
@@ -0,0 +1,5 @@
import { VerticalBox } from "std-widgets.slint";
export component Page inherits VerticalBox {
padding: 20px;
alignment: start;
}
+9
View File
@@ -0,0 +1,9 @@
import { Page } from "../page.slint";
export component ClientsPage inherits Page {
Text {
text: "Clients";
font-size: 18px;
font-weight: 700;
}
}
+3
View File
@@ -0,0 +1,3 @@
import { Page } from "../page.slint";
export component DashboardPage inherits Page { }
+64
View File
@@ -0,0 +1,64 @@
import { Page } from "../page.slint";
import { UITcpListener } from "../structs.slint";
import { ScrollView, GridBox, Button } from "std-widgets.slint";
import { BorderedRectangle, BoolText, TitleText } from "../components.slint";
component ListenerCard inherits BorderedRectangle {
in property <UITcpListener> listener;
VerticalLayout {
padding: 10px;
Text {
text: listener.name;
font-weight: 700;
font-size: 18px;
}
TitleText {
text: "Enabled: ";
BoolText {
state: listener.enabled;
font-weight: 500;
font-size: 14px;
}
}
TitleText {
text: "Remote Host: ";
Text {
text: listener.remote-host;
font-weight: 500;
font-size: 14px;
}
}
TitleText {
text: "Port: ";
Text {
text: listener.port;
font-weight: 500;
font-size: 14px;
}
}
}
}
export component ListenersPage inherits Page {
in-out property <[UITcpListener]> listeners;
ScrollView {
width: 100%;
height: 100%;
VerticalLayout {
for listener[i] in listeners: ListenerCard {
listener: listener;
}
Rectangle { }
}
}
}
+3
View File
@@ -0,0 +1,3 @@
import { Page } from "../page.slint";
export component ToolsPage inherits Page { }
+6
View File
@@ -0,0 +1,6 @@
export struct UITcpListener {
enabled: bool,
name: string,
remote_host: string,
port: int,
}
+9
View File
@@ -0,0 +1,9 @@
[package]
name = "unshell-rs-lib"
edition = "2024"
[dependencies]
base64 = "0.22.1"
log = "0.4.27"
serde = { version = "1.0.219", features = ["derive"] }
serde_json = "1.0.140"
+6
View File
@@ -0,0 +1,6 @@
use serde::{Deserialize, Serialize};
#[derive(Debug, Serialize, Deserialize)]
pub struct CampignConfig {
name: String,
}
+4
View File
@@ -0,0 +1,4 @@
use serde::{Deserialize, Serialize};
#[derive(Debug, Serialize, Deserialize)]
pub enum LayerConfig {}
+14
View File
@@ -0,0 +1,14 @@
use serde::{Deserialize, Serialize};
use crate::config::layers::LayerConfig;
#[derive(Debug, Serialize, Deserialize)]
pub enum ListenerConfig {
Tcp {
enabled: bool,
name: String,
remote_host: String,
port: u16,
layers: Vec<LayerConfig>,
},
}
+3
View File
@@ -0,0 +1,3 @@
pub mod campaign;
pub mod layers;
pub mod listeners;
@@ -1,7 +1,8 @@
use crate::layers::Layer; use crate::layers::Layer;
use base64; use base64;
use serde::{Deserialize, Serialize};
#[derive(Default)] #[derive(Default, Serialize, Deserialize)]
pub struct Base64; pub struct Base64;
impl Layer for Base64 { impl Layer for Base64 {
@@ -1,4 +1,4 @@
pub trait Layer { pub trait Layer: Serialize + Deserialize<'static> + Sized {
fn encode(&mut self, data: &[u8]) -> Vec<u8>; fn encode(&mut self, data: &[u8]) -> Vec<u8>;
fn decode(&mut self, data: &[u8]) -> Vec<u8>; fn decode(&mut self, data: &[u8]) -> Vec<u8>;
} }
@@ -6,3 +6,4 @@ pub trait Layer {
pub mod base64; pub mod base64;
pub use base64::Base64; pub use base64::Base64;
use serde::{Deserialize, Serialize};
+8
View File
@@ -0,0 +1,8 @@
#[macro_use]
extern crate log;
pub mod config;
pub mod layers;
pub mod listeners;
pub mod networkers;
pub mod packets;
@@ -1,3 +1,5 @@
use crate::layers::Layer;
mod client; mod client;
mod server; mod server;
@@ -1,15 +1,8 @@
use log::{info, trace, warn}; use std::sync::{Arc, Mutex};
use std::{
io::{self, Write},
sync::{Arc, Mutex},
thread,
};
use crate::{ use crate::{
listeners::client::{self, Client}, listeners::client::Client,
networkers::{Connection, ServerTrait}, networkers::{Connection, ServerTrait},
packets::Packet,
}; };
pub struct Listener<S, C> { pub struct Listener<S, C> {
@@ -41,7 +34,7 @@ impl<S, C> Listener<S, C> {
clients_lock.push(Client::new(conn)); clients_lock.push(Client::new(conn));
} }
Err(e) => { Err(e) => {
eprintln!("Failed to accept connection: {:?}", e); error!("Failed to accept connection: {:?}", e);
} }
} }
} }