From c015aa115dedc0b9aac37ea1889f01369cf345a3 Mon Sep 17 00:00:00 2001 From: 53hornet Date: Sun, 28 Nov 2021 10:51:44 -0500 Subject: feat: publish tarpit, new drafts --- .gitignore | 2 + ...-your-own-ssh-tarpit-in-rust-with-async-std.php | 192 ---------- drafts/2021-11-19-agile-is-not-an-excuse.php | 18 + drafts/your company isnt doing SRE.php | 4 + ...-your-own-ssh-tarpit-in-rust-with-async-std.php | 393 +++++++++++++++++++++ 5 files changed, 417 insertions(+), 192 deletions(-) create mode 100644 .gitignore delete mode 100644 drafts/2021-10-06-write-your-own-ssh-tarpit-in-rust-with-async-std.php create mode 100644 drafts/2021-11-19-agile-is-not-an-excuse.php create mode 100644 posts/2021-11-28-write-your-own-ssh-tarpit-in-rust-with-async-std.php diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..ae2530e --- /dev/null +++ b/.gitignore @@ -0,0 +1,2 @@ +*.core +TODO.txt diff --git a/drafts/2021-10-06-write-your-own-ssh-tarpit-in-rust-with-async-std.php b/drafts/2021-10-06-write-your-own-ssh-tarpit-in-rust-with-async-std.php deleted file mode 100644 index 26824da..0000000 --- a/drafts/2021-10-06-write-your-own-ssh-tarpit-in-rust-with-async-std.php +++ /dev/null @@ -1,192 +0,0 @@ - - -

- A software tarpit is simple and fun. Long story short, it's sort of a reverse denial-of-service attack. It usually works by inserting an intentional, arbitrary delay in responding to malicious clients, thus wasting their time and resources. It's kind of like those YouTubers who purposely joke around with phone scammers as long as possible to waste their time and have fun. I recently learned about endlessh, an SSH tarpit. I decided it would be a fun exercise to use Rust's async-std library to write an SSH tarpit of my own, with my own personal flair. If you want to learn more about endlessh or SSH tarpits I highly recommend reading this blog post by the endlessh author. -

- -

Goals

- -

- So what does an SSH tarpit need to do? Basically, an SSH tarpit is a TCP listener that very slowly writes a never-ending SSH banner back to the client. This all happens pre-login, so no fancy SSH libraries are needed. Really, the program just needs to write bytes [slowly] back to the incoming connection and never offer up the chance to authenticate (see the blog post above to learn more). So now I'm getting a to-do list together. My program needs to... -

- -
    -
  1. Listen for incoming TCP connections on a given port
  2. -
  3. Upon receiving an incoming connection, write some data to the TCP stream
  4. -
  5. Wait a little bit (say 1-10 seconds) and then repeat step 2
  6. -
  7. Upon client disconnect, continue working on other connections and listening for new ones
  8. -
  9. Handle many of these connections at the same time with few resources
  10. -
- -

- Additionally, to spruce things up and have more fun, I'm adding the following requirements: -

- - - -

- That's right. It's probably a waste of resources, but I want to be able to feed the attacker whatever information I want. For example, I want to be able to pipe a Unix Fortune across the network to the attacker very slowly. I want to relish in the knowledge that if the attacker manually debugs the data coming down the pipe, they'll see their fortune. -

- -

Implementation

- -

- I've chosen Rust and the recently-stabilized async-std library for a variety of reasons. First, I like Rust. It's a good language. Second, async-std offers an asynchronous task-switching runtime much like Python's asyncio, or even JavaScript's Promise (even though things work a little differently under the hood). Long story short, it allows for easy concurrent programming with few resources and high performance. Everything I could want, really. It also comes with a variety of useful standard library API features reimplemented as asynchronous tasks. -

- -

- For starters, I need to include async-std as a dependency. I'm also going to pull in anyhow for friendlier error handling. -

- -

- Cargo.toml: -

-
-[package]
-name = "fortune-pit"
-version = "0.1.0"
-edition = "2018"
-
-[dependencies.async-std]
-version = "1"
-features = ["attributes"]
-
-[dependencies.anyhow]
-version = "1"
-
-
-

- -

- Now I've gotta actually write some code. I already said I wanted to listen for incoming TCP connections, so I'll start with a TcpListener. - -

-
-use anyhow::Result;
-use async_std::net::TcpListener;
-
-#[async_std::main]
-async fn main() -> Result<()> {
-    let listener = TcpListener::bind("0.0.0.0:2222").await?;
-    Ok(())
-}
-
-
- -Better yet, I'll make the bind port configurable at runtime. In a live environment, it would be best to run this on port 22. But for testing purposes, as a non-root user, I'll run it on 2222. These changes will let me do that at will. I'll also print a nicer error message if anything goes wrong here. -

- -

-

-
-use anyhow::{Context, Result};
-use async_std::net::{Ipv4Addr, SocketAddrV4, TcpListener};
-use std::env;
-
-#[async_std::main]
-async fn main() -> Result<()> {
-    let listener = TcpListener::bind(read_addr()?)
-        .await
-        .with_context(|| "tarpit: failed to bind TCP listener")?;
-    Ok(())
-}
-
-fn read_addr() -> Result {
-    let port = env::args()
-        .nth(1)
-        .map(|arg| arg.parse())
-        .unwrap_or(Ok(22))
-        .with_context(|| "tarpit: failed to parse bind port")?;
-
-    Ok(SocketAddrV4::new(Ipv4Addr::new(0, 0, 0, 0), port))
-}
-
-
- -There. Slightly more complicated, but much more convenient. -

- -

- Now I need to actually do something with my TcpListener. I'll loop over incoming connections and open streams for all of them. Then I'll write something to those streams and close them. - -

-
-use anyhow::{Context, Result};
-use async_std::{
-    io::prelude::*,
-    net::{Ipv4Addr, SocketAddrV4, TcpListener},
-    prelude::*,
-};
-use std::env;
-
-#[async_std::main]
-async fn main() -> Result<()> {
-    let listener = TcpListener::bind(read_addr()?)
-        .await
-        .with_context(|| "tarpit: failed to bind TCP listener")?;
-
-    let mut incoming = listener.incoming();
-    while let Some(stream) = incoming.next().await {
-        let mut stream = stream?;
-        writeln!(stream, "Here's your fortune!").await?;
-    }
-
-    Ok(())
-}
-
-fn read_addr() -> Result {
-    let port = env::args()
-        .nth(1)
-        .map(|arg| arg.parse())
-        .unwrap_or(Ok(22))
-        .with_context(|| "tarpit: failed to parse bind port")?;
-
-    Ok(SocketAddrV4::new(Ipv4Addr::new(0, 0, 0, 0), port))
-}
-
-
- -And it works! If I cargo run -- 2222, passing my port as the first argument, I get a very basic TCP server. I can test it with nc(1). - -
-
-$ nc localhost 2222
-Here's your fortune!
-^C
-
-
-

- -

- Great. But not there yet. First of all, my client immediately receives a response. I want to keep feeding information to the client over and over until it gives up. I don't want it to time out waiting for nothing. I also want the user to pick what gets written. -

- -

- So let's read in whatever the user wants to send along on STDIN. I thought this would be better than reading a file because it's really easy to send files to STDIN with pipes or redirection. It also lets you write the output of commands like fortune(6). - -

-
-
-
-

- -

Improvements

- -

- The SSH RFC specifies that lines written for the banner cannot exceed 255 characters including carriage return and line feed. This program imposes no such restriction, although it would probably be fairly easy to break up incoming text into 255 character lines and write those one word at a time. -

- -

- It's probably also worth noting that should any lines begin with SSH-, they're interpreted as SSH protocol version identifiers, causing the client and server to begin their authentication dance. Normally, unless the following data is valid, this results in a disconnect. In a worst-case scenario, if you pipe a completely legitimate protocol version, the client will successfully attempt authentication. -

diff --git a/drafts/2021-11-19-agile-is-not-an-excuse.php b/drafts/2021-11-19-agile-is-not-an-excuse.php new file mode 100644 index 0000000..24afa4f --- /dev/null +++ b/drafts/2021-11-19-agile-is-not-an-excuse.php @@ -0,0 +1,18 @@ + + +Don't use agile software development as a crutch or an excuse for not adding value. Reacting to change and pivoting for your stakeholders only works when you go from one "swimlane" or "track" to another. It doesn't work when you move cyclically from one project to another whenever one seems more important than the other to you, management, stakeholders, etc. + +Don't use agile software development as the excuse to have stories "done" in a certain number of sprints. Quarterly predictions should not confine the process to days or weeks. + +Remember: + +- Individuals and interactions over processes and tools +- Working software over comprehensive documentation +- Customer collaboration over contract negotiation +- Responding to change over following a plan diff --git a/drafts/your company isnt doing SRE.php b/drafts/your company isnt doing SRE.php index fdb1e5e..87ebcae 100644 --- a/drafts/your company isnt doing SRE.php +++ b/drafts/your company isnt doing SRE.php @@ -2,3 +2,7 @@ your company probably isn't doing SRE Google SRE - embracing risk - 50% time is for free thought and development if your "devops" are so busy doing ticket work they're asking for you to hire, you're not doing SRE https://sre.google/sre-book/introduction/ + +Don't forget the chapter that says 50% time for development + +Don't forget the one that says no new deployments when ops are at capacity. "But the business needs this" well then the business needs to hire more people. diff --git a/posts/2021-11-28-write-your-own-ssh-tarpit-in-rust-with-async-std.php b/posts/2021-11-28-write-your-own-ssh-tarpit-in-rust-with-async-std.php new file mode 100644 index 0000000..54e2f83 --- /dev/null +++ b/posts/2021-11-28-write-your-own-ssh-tarpit-in-rust-with-async-std.php @@ -0,0 +1,393 @@ + + +

+ A software tarpit is simple and fun. Long story short, it's sort of a reverse denial-of-service attack. It usually works by inserting an intentional, arbitrary delay in responding to malicious clients, thus wasting their time and resources. It's kind of like those YouTubers who purposely joke around with phone scammers as long as possible to waste their time and have fun. I recently learned about endlessh, an SSH tarpit. I decided it would be a fun exercise to use Rust's async-std library to write an SSH tarpit of my own, with my own personal flair. If you want to learn more about endlessh or SSH tarpits I highly recommend reading this blog post by the endlessh author. +

+ +

Goals

+ +

+ So what does an SSH tarpit need to do? Basically, an SSH tarpit is a TCP listener that very slowly writes a never-ending SSH banner back to the client. This all happens pre-login, so no fancy SSH libraries are needed. Really, the program just needs to write bytes [slowly] back to the incoming connection and never offer up the chance to authenticate (see the blog post above to learn more). So now I'm getting a to-do list together. My program needs to... +

+ +
    +
  1. Listen for incoming TCP connections on a given port
  2. +
  3. Upon receiving an incoming connection, write some data to the TCP stream
  4. +
  5. Wait a little bit (say 1-10 seconds) and then repeat step 2
  6. +
  7. Upon client disconnect, continue working on other connections and listening for new ones
  8. +
  9. Handle many of these connections at the same time with few resources
  10. +
+ +

+ Additionally, to spruce things up and have more fun, I'm adding the following requirements: +

+ + + +

+ That's right. It's probably a waste of resources, but I want to be able to feed the attacker whatever information I want. For example, I want to be able to pipe a Unix Fortune across the network to the attacker very slowly. I want to relish in the knowledge that if the attacker manually debugs the data coming down the pipe, they'll see their fortune. +

+ +

Implementation

+ +

+ I've chosen Rust and the recently-stabilized async-std library for a variety of reasons. First, I like Rust. It's a good language. Second, async-std offers an asynchronous task-switching runtime much like Python's asyncio, or even JavaScript's Promise (even though things work a little differently under the hood). Long story short, it allows for easy concurrent programming with few resources and high performance. Everything I could want, really. It also comes with a variety of useful standard library API features reimplemented as asynchronous tasks. +

+ +

+ For starters, I need to include async-std as a dependency. I'm also going to pull in anyhow for friendlier error handling. +

+ +

+ Cargo.toml: +

+
+[package]
+name = "fortune-pit"
+version = "0.1.0"
+edition = "2018"
+
+[dependencies.async-std]
+version = "1"
+features = ["attributes"]
+
+[dependencies.anyhow]
+version = "1"
+
+
+

+ +

+ Now I've gotta actually write some code. I already said I wanted to listen for incoming TCP connections, so I'll start with a TcpListener. + +

+
+use anyhow::Result;
+use async_std::net::TcpListener;
+
+#[async_std::main]
+async fn main() -> Result<()> {
+    let listener = TcpListener::bind("0.0.0.0:2222").await?;
+    Ok(())
+}
+
+
+ +Better yet, I'll make the bind port configurable at runtime. In a live environment, it would be best to run this on port 22. But for testing purposes, as a non-root user, I'll run it on 2222. These changes will let me do that at will. I'll also print a nicer error message if anything goes wrong here. +

+ +

+

+
+use anyhow::{Context, Result};
+use async_std::net::{Ipv4Addr, SocketAddrV4, TcpListener};
+use std::env;
+
+#[async_std::main]
+async fn main() -> Result<()> {
+    let listener = TcpListener::bind(read_addr()?)
+        .await
+        .with_context(|| "tarpit: failed to bind TCP listener")?;
+    Ok(())
+}
+
+fn read_addr() -> Result {
+    let port = env::args()
+        .nth(1)
+        .map(|arg| arg.parse())
+        .unwrap_or(Ok(22))
+        .with_context(|| "tarpit: failed to parse bind port")?;
+
+    Ok(SocketAddrV4::new(Ipv4Addr::new(0, 0, 0, 0), port))
+}
+
+
+ +There. Slightly more complicated, but much more convenient. +

+ +

+ Now I need to actually do something with my TcpListener. I'll loop over incoming connections and open streams for all of them. Then I'll write something to those streams and close them. + +

+
+use anyhow::{Context, Result};
+use async_std::{
+    io::prelude::*,
+    net::{Ipv4Addr, SocketAddrV4, TcpListener},
+    prelude::*,
+};
+use std::env;
+
+#[async_std::main]
+async fn main() -> Result<()> {
+    let listener = TcpListener::bind(read_addr()?)
+        .await
+        .with_context(|| "tarpit: failed to bind TCP listener")?;
+
+    let mut incoming = listener.incoming();
+    while let Some(stream) = incoming.next().await {
+        let mut stream = stream?;
+        writeln!(stream, "Here's your fortune!").await?;
+    }
+
+    Ok(())
+}
+
+fn read_addr() -> Result {
+    let port = env::args()
+        .nth(1)
+        .map(|arg| arg.parse())
+        .unwrap_or(Ok(22))
+        .with_context(|| "tarpit: failed to parse bind port")?;
+
+    Ok(SocketAddrV4::new(Ipv4Addr::new(0, 0, 0, 0), port))
+}
+
+
+ +And it works! If I cargo run -- 2222, passing my port as the first argument, I get a very basic TCP server. I can test it with nc(1). + +
+
+$ nc localhost 2222
+Here's your fortune!
+^C
+
+
+

+ +

+ Great. But not there yet. First of all, I want the user to pick what gets written. Second, my client immediately receives a response. I want to keep feeding information to the client over and over until it gives up. I don't want it to time out waiting for nothing. +

+ +

+ So let's read in whatever the user wants to send along on STDIN. I thought this would be better than reading a file because it's really easy to send files to STDIN with pipes or redirection. It also lets you write the output of commands like fortune(6). + +

+
+async fn read_banner() -> Result {
+    let mut banner = String::new();
+    io::stdin()
+        .read_to_string(&mut banner)
+        .await
+        .with_context(|| "tarpit: failed to read banner")?;
+    Ok(banner)
+}
+
+
+ +I can then read that banner in main(): + +
+
+let banner = read_banner().await?;
+
+
+

+ +

+ Now comes the tarpit part, continuously writing that banner to the client slowly until they disconnect. I'll write a handler() function that will spawn with every new client connection instead of doing everything in the closure in main(). The handler will accept a new client's TcpStream and a borrow-able reference to the banner. The first thing I'll do is read the client's address. It's more fun if we can see who is connected and how long they've been stuck. Then I'll log that to STDERR. Finally, I'll loop over every whitespace-separated word in the banner forever, writing a single word at a time to the stream and sleeping between each word for a predefined duration. If I can no longer write to the client, I'll assume they've disconnected and bail out. We don't want our tarpit to trap our system in itself. + +

+
+static DURATION: Duration = Duration::from_secs(1);
+
+async fn handler(mut stream: TcpStream, banner: Arc) {
+    let peer_addr = stream
+        .peer_addr()
+        .map(|p| p.to_string())
+        .unwrap_or_else(|_| String::from("(unknown)"));
+    eprintln!("connected\t{}", peer_addr);
+    for word in banner.split_inclusive(char::is_whitespace).cycle() {
+        if stream.write_all(word.as_bytes()).await.is_err() {
+            eprintln!("disconnected\t{}", peer_addr);
+            return;
+        }
+        task::sleep(DURATION).await;
+    }
+}
+
+
+ +Note: I'm using an Arc so that all of my asynchronous tasks can read from the same underlying banner data instead of cloning it. +

+ +

Altogether Now

+ +Here is the entire program (click for Git). + +
+
+use anyhow::{Context, Result};
+use async_std::{
+    io,
+    io::prelude::*,
+    net::{Ipv4Addr, SocketAddrV4, TcpListener, TcpStream},
+    prelude::*,
+    task,
+};
+use std::{env, sync::Arc, time::Duration};
+
+static DURATION: Duration = Duration::from_secs(1);
+
+/// Listens for incoming TCP connections and handles them.
+#[async_std::main]
+async fn main() -> Result<()> {
+    let listener = TcpListener::bind(read_addr()?)
+        .await
+        .with_context(|| "tarpit: failed to bind TCP listener")?;
+
+    let banner = Arc::new(read_banner().await?);
+
+    let mut incoming = listener.incoming();
+    while let Some(stream) = incoming.next().await {
+        match stream {
+            Err(e) => {
+                eprintln!("error\t{}", e);
+                continue;
+            }
+            Ok(s) => {
+                let banner = banner.clone();
+                task::spawn(handler(s, banner));
+            }
+        }
+    }
+
+    Ok(())
+}
+
+/// Handles a single TCP connection, continuously writing banner to the TCP stream sloooooowly.
+async fn handler(mut stream: TcpStream, banner: Arc) {
+    let peer_addr = stream
+        .peer_addr()
+        .map(|p| p.to_string())
+        .unwrap_or_else(|_| String::from("(unknown)"));
+
+    eprintln!("connected\t{}", peer_addr);
+
+    for word in banner.split_inclusive(char::is_whitespace).cycle() {
+        if stream.write_all(word.as_bytes()).await.is_err() {
+            eprintln!("disconnected\t{}", peer_addr);
+            return;
+        }
+        task::sleep(DURATION).await;
+    }
+}
+
+/// Reads address port to bind to from first arguent.
+fn read_addr() -> Result {
+    let port = env::args()
+        .nth(1)
+        .map(|arg| arg.parse())
+        .unwrap_or(Ok(22))
+        .with_context(|| "tarpit: failed to parse bind port")?;
+
+    Ok(SocketAddrV4::new(Ipv4Addr::new(0, 0, 0, 0), port))
+}
+
+/// Reads banner to be written to client from STDIN.
+async fn read_banner() -> Result {
+    let mut banner = String::new();
+    io::stdin()
+        .read_to_string(&mut banner)
+        .await
+        .with_context(|| "tarpit: failed to read banner")?;
+    Ok(banner)
+}
+
+
+ +

Execution

+ +

+ Now let's have a laugh. For my banner, I've selected the entirety of Michael Crichton's Jurassic Park: + +

+
+$ head jp.txt
+"The InGen Incident"
+
+The late twentieth century has witnessed a scientific gold rush of astonishing proportions: the
+headlong and furious haste to commercialize genetic engineering. This enterprise has proceeded
+so rapidly-with so little outside commentary-that its dimensions and implications are hardly
+understood at all.
+Biotechnology promises the greatest revolution in human history. By the end of this decade, it
+will have outdistanced atomic power and computers in its effect on our everyday lives. In the
+words of one observer, "Biotechnology is going to transform every aspect of human life: our
+medical care, our food, our health, our entertainment, our very bodies. Nothing will ever be the
+
+
+

+ +

+ I'll run the tarpit on my localhost, and bind to port 22. + +

+
+# ./fortune-pit 22 < jp.txt
+
+
+ +And then on another host, I'll use OpenSSH to connect to my tarpit: + +
+
+ssh 192.168.1.3 -v -p 22
+OpenSSH_7.9p1, OpenSSL 1.1.1k-freebsd  24 Aug 2021
+...
+debug1: ssh_exchange_identification: "The InGen Incident"
+debug1: ssh_exchange_identification:
+debug1: ssh_exchange_identification: The late twentieth century has witnessed a scientific gold rush of astonishing proportions: the
+debug1: ssh_exchange_identification: headlong and furious haste to commercialize genetic engineering. This enterprise has proceeded
+debug1: ssh_exchange_identification: so rapidly-with so little outside commentary-that its dimensions and implications are hardly
+debug1: ssh_exchange_identification: understood at all.
+debug1: ssh_exchange_identification: Biotechnology promises the greatest revolution in human history. By the end of this decade, it
+
+
+ +And presto, our SSH client is trapped reading Jurassic forever. Note that OpenSSH is flushing debugging lines on the exchange ID with every newline. If your banner has no newlines, you may not see this until the buffer fills (if at all). We can quit with Ctrl+C and take a look at our "logs": + +
+
+# ./fortune-pit 22 < jp.txt
+connected       192.168.1.2:41826
+disconnected    192.168.1.2:41826
+
+
+

+ +

Improvements

+ +

+ The SSH RFC specifies that lines written for the banner cannot exceed 255 characters including carriage return and line feed. This program imposes no such restriction, although it would probably be fairly easy to break up incoming text into 255 character lines and write those one word at a time. +

+ +

+ It's probably also worth noting that should any lines begin with SSH-, they're interpreted as SSH protocol version identifiers, causing the client and server to begin their authentication dance. Normally, unless the following data is valid, this results in a disconnect. In a worst-case scenario, if you pipe a completely legitimate protocol version, the client will successfully attempt authentication. +

+ +

A Security Feature or a Toy?

+ +

+ I will never run this on a production system and leave it there. It's a curiosity, not a security feature. I have a couple of reasons for this. First, I have no business writing homebrew software that stays running on TCP 22 all day every day. That's just not something I trust myself with or want to troubleshoot and maintain. +

+ +

+ Second, there is an argument going around that this is beneficial because it "wastes time and resources for attackers." This is a fallacy. We can't save the world by trolling attackers, no matter what Reddit tells you. Why? Botnets. For example, as an experiment, I did run this on a production, web-facing server for about a week. By the end I had lots of connects and disconnects from all over the world, and about 20 or so clients were still trapped in the tarpit when I killed it. Some of them were from known malicious Russian hosts, but more often than not, they were random. The awful reality is these trapped hosts were probably end-users' malware-infected hosts being bought and sold into botnets around the world. Trapping them and tying up their resources is a cold way to get back at an attacker that will just go and buy more bots, or write better malware to hang up when they discover tarpits like this one. +

+ +

+ So enjoy the tarpit, make it your own, use it to screw with your friends if you share a server. But use a good firewall and leave the guerilla web defense protocols idea behind. +

-- cgit v1.2.3