eris: support regex patterns; add TOML config support
This commit is contained in:
parent
637548aa86
commit
cb2f106239
3 changed files with 275 additions and 32 deletions
235
src/main.rs
235
src/main.rs
|
@ -1,6 +1,7 @@
|
|||
use actix_web::{App, HttpResponse, HttpServer, web};
|
||||
use clap::Parser;
|
||||
use ipnetwork::IpNetwork;
|
||||
use regex::Regex;
|
||||
use serde::{Deserialize, Serialize};
|
||||
use std::collections::{HashMap, HashSet};
|
||||
use std::env;
|
||||
|
@ -96,7 +97,7 @@ struct Args {
|
|||
|
||||
#[clap(
|
||||
long,
|
||||
help = "Path to JSON configuration file (overrides command line options)"
|
||||
help = "Path to configuration file (JSON or TOML, overrides command line options)"
|
||||
)]
|
||||
config_file: Option<PathBuf>,
|
||||
|
||||
|
@ -108,6 +109,44 @@ struct Args {
|
|||
log_level: String,
|
||||
}
|
||||
|
||||
// Trap pattern structure that can be either a plain string or regex
|
||||
#[derive(Clone, Debug, Deserialize, Serialize)]
|
||||
#[serde(untagged)]
|
||||
enum TrapPattern {
|
||||
Plain(String),
|
||||
Regex { pattern: String, regex: bool },
|
||||
}
|
||||
|
||||
impl TrapPattern {
|
||||
fn as_plain(value: &str) -> Self {
|
||||
Self::Plain(value.to_string())
|
||||
}
|
||||
|
||||
fn as_regex(value: &str) -> Self {
|
||||
Self::Regex {
|
||||
pattern: value.to_string(),
|
||||
regex: true,
|
||||
}
|
||||
}
|
||||
|
||||
fn matches(&self, path: &str) -> bool {
|
||||
match self {
|
||||
Self::Plain(pattern) => path.contains(pattern),
|
||||
Self::Regex {
|
||||
pattern,
|
||||
regex: true,
|
||||
} => {
|
||||
if let Ok(re) = Regex::new(pattern) {
|
||||
re.is_match(path)
|
||||
} else {
|
||||
false
|
||||
}
|
||||
}
|
||||
_ => false,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Configuration structure
|
||||
#[derive(Clone, Debug, Deserialize, Serialize)]
|
||||
struct Config {
|
||||
|
@ -119,7 +158,7 @@ struct Config {
|
|||
max_delay: u64,
|
||||
max_tarpit_time: u64,
|
||||
block_threshold: u32,
|
||||
trap_patterns: Vec<String>,
|
||||
trap_patterns: Vec<TrapPattern>,
|
||||
whitelist_networks: Vec<String>,
|
||||
markov_corpora_dir: String,
|
||||
lua_scripts_dir: String,
|
||||
|
@ -140,17 +179,37 @@ impl Default for Config {
|
|||
max_tarpit_time: 600,
|
||||
block_threshold: 3,
|
||||
trap_patterns: vec![
|
||||
"/vendor/phpunit".to_string(),
|
||||
"eval-stdin.php".to_string(),
|
||||
"/wp-admin".to_string(),
|
||||
"/wp-login.php".to_string(),
|
||||
"/xmlrpc.php".to_string(),
|
||||
"/phpMyAdmin".to_string(),
|
||||
"/solr/".to_string(),
|
||||
"/.env".to_string(),
|
||||
"/config".to_string(),
|
||||
"/api/".to_string(),
|
||||
"/actuator/".to_string(),
|
||||
// Basic attack patterns as plain strings
|
||||
TrapPattern::as_plain("/vendor/phpunit"),
|
||||
TrapPattern::as_plain("eval-stdin.php"),
|
||||
TrapPattern::as_plain("/wp-admin"),
|
||||
TrapPattern::as_plain("/wp-login.php"),
|
||||
TrapPattern::as_plain("/xmlrpc.php"),
|
||||
TrapPattern::as_plain("/phpMyAdmin"),
|
||||
TrapPattern::as_plain("/solr/"),
|
||||
TrapPattern::as_plain("/.env"),
|
||||
TrapPattern::as_plain("/config"),
|
||||
TrapPattern::as_plain("/actuator/"),
|
||||
// More aggressive patterns for various PHP exploits
|
||||
TrapPattern::as_regex(r"/.*phpunit.*eval-stdin\.php"),
|
||||
TrapPattern::as_regex(r"/index\.php\?s=/index/\\think\\app/invokefunction"),
|
||||
TrapPattern::as_regex(r".*%ADd\+auto_prepend_file%3dphp://input.*"),
|
||||
TrapPattern::as_regex(r".*%ADd\+allow_url_include%3d1.*"),
|
||||
TrapPattern::as_regex(r".*/wp-content/plugins/.*\.php"),
|
||||
TrapPattern::as_regex(r".*/wp-content/themes/.*\.php"),
|
||||
TrapPattern::as_regex(r".*eval\(.*\).*"),
|
||||
TrapPattern::as_regex(r".*/adminer\.php.*"),
|
||||
TrapPattern::as_regex(r".*/admin\.php.*"),
|
||||
TrapPattern::as_regex(r".*/administrator/.*"),
|
||||
TrapPattern::as_regex(r".*/wp-json/.*"),
|
||||
TrapPattern::as_regex(r".*/api/.*\.php.*"),
|
||||
TrapPattern::as_regex(r".*/cgi-bin/.*"),
|
||||
TrapPattern::as_regex(r".*/owa/.*"),
|
||||
TrapPattern::as_regex(r".*/ecp/.*"),
|
||||
TrapPattern::as_regex(r".*/webshell\.php.*"),
|
||||
TrapPattern::as_regex(r".*/shell\.php.*"),
|
||||
TrapPattern::as_regex(r".*/cmd\.php.*"),
|
||||
TrapPattern::as_regex(r".*/struts.*"),
|
||||
],
|
||||
whitelist_networks: vec![
|
||||
"192.168.0.0/16".to_string(),
|
||||
|
@ -234,19 +293,65 @@ impl Config {
|
|||
}
|
||||
}
|
||||
|
||||
// Load configuration from a JSON file
|
||||
// Load configuration from a file (JSON or TOML)
|
||||
fn load_from_file(path: &Path) -> std::io::Result<Self> {
|
||||
let content = fs::read_to_string(path)?;
|
||||
let config = serde_json::from_str(&content)?;
|
||||
|
||||
let extension = path
|
||||
.extension()
|
||||
.map(|ext| ext.to_string_lossy().to_lowercase())
|
||||
.unwrap_or_default();
|
||||
|
||||
let config = match extension.as_str() {
|
||||
"toml" => toml::from_str(&content).map_err(|e| {
|
||||
std::io::Error::new(
|
||||
std::io::ErrorKind::InvalidData,
|
||||
format!("Failed to parse TOML: {e}"),
|
||||
)
|
||||
})?,
|
||||
_ => {
|
||||
// Default to JSON for any other extension
|
||||
serde_json::from_str(&content).map_err(|e| {
|
||||
std::io::Error::new(
|
||||
std::io::ErrorKind::InvalidData,
|
||||
format!("Failed to parse JSON: {e}"),
|
||||
)
|
||||
})?
|
||||
}
|
||||
};
|
||||
|
||||
Ok(config)
|
||||
}
|
||||
|
||||
// Save configuration to a JSON file
|
||||
// Save configuration to a file (JSON or TOML)
|
||||
fn save_to_file(&self, path: &Path) -> std::io::Result<()> {
|
||||
if let Some(parent) = path.parent() {
|
||||
fs::create_dir_all(parent)?;
|
||||
}
|
||||
let content = serde_json::to_string_pretty(self)?;
|
||||
|
||||
let extension = path
|
||||
.extension()
|
||||
.map(|ext| ext.to_string_lossy().to_lowercase())
|
||||
.unwrap_or_default();
|
||||
|
||||
let content = match extension.as_str() {
|
||||
"toml" => toml::to_string_pretty(self).map_err(|e| {
|
||||
std::io::Error::new(
|
||||
std::io::ErrorKind::InvalidData,
|
||||
format!("Failed to serialize to TOML: {e}"),
|
||||
)
|
||||
})?,
|
||||
_ => {
|
||||
// Default to JSON for any other extension
|
||||
serde_json::to_string_pretty(self).map_err(|e| {
|
||||
std::io::Error::new(
|
||||
std::io::ErrorKind::InvalidData,
|
||||
format!("Failed to serialize to JSON: {e}"),
|
||||
)
|
||||
})?
|
||||
}
|
||||
};
|
||||
|
||||
fs::write(path, content)?;
|
||||
Ok(())
|
||||
}
|
||||
|
@ -394,7 +499,7 @@ fn extract_path_from_request(data: &[u8]) -> Option<&str> {
|
|||
.next()
|
||||
.filter(|line| !line.is_empty())?;
|
||||
|
||||
// Split by spaces and ensure we have at least 3 parts
|
||||
// Split by spaces and ensure we have at least 3 parts (METHOD PATH VERSION)
|
||||
let parts: Vec<&[u8]> = first_line.split(|&b| b == b' ').collect();
|
||||
if parts.len() < 3 || !parts[2].starts_with(b"HTTP/") {
|
||||
return None;
|
||||
|
@ -886,10 +991,9 @@ async fn should_tarpit(path: &str, ip: &IpAddr, config: &Config) -> bool {
|
|||
}
|
||||
}
|
||||
|
||||
// Use a more efficient pattern matching approach
|
||||
let path_lower = path.to_lowercase();
|
||||
// Use pattern matching based on the trap pattern type (plain string or regex)
|
||||
for pattern in &config.trap_patterns {
|
||||
if path_lower.contains(pattern) {
|
||||
if pattern.matches(path) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
@ -1092,12 +1196,23 @@ async fn main() -> std::io::Result<()> {
|
|||
if let Err(e) = fs::create_dir_all(&config.config_dir) {
|
||||
log::warn!("Failed to create config directory: {e}");
|
||||
} else {
|
||||
let config_path = Path::new(&config.config_dir).join("config.json");
|
||||
if !config_path.exists() {
|
||||
if let Err(e) = config.save_to_file(&config_path) {
|
||||
log::warn!("Failed to save default configuration: {e}");
|
||||
// Save both JSON and TOML versions of the config for user reference
|
||||
let config_path_json = Path::new(&config.config_dir).join("config.json");
|
||||
let config_path_toml = Path::new(&config.config_dir).join("config.toml");
|
||||
|
||||
if !config_path_json.exists() {
|
||||
if let Err(e) = config.save_to_file(&config_path_json) {
|
||||
log::warn!("Failed to save JSON configuration: {e}");
|
||||
} else {
|
||||
log::info!("Saved default configuration to {config_path:?}");
|
||||
log::info!("Saved JSON configuration to {config_path_json:?}");
|
||||
}
|
||||
}
|
||||
|
||||
if !config_path_toml.exists() {
|
||||
if let Err(e) = config.save_to_file(&config_path_toml) {
|
||||
log::warn!("Failed to save TOML configuration: {e}");
|
||||
} else {
|
||||
log::info!("Saved TOML configuration to {config_path_toml:?}");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -1318,6 +1433,27 @@ mod tests {
|
|||
assert_eq!(config.cache_dir, "/tmp/eris/cache");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_trap_pattern_matching() {
|
||||
// Test plain string pattern
|
||||
let plain = TrapPattern::as_plain("phpunit");
|
||||
assert!(plain.matches("path/to/phpunit/test"));
|
||||
assert!(!plain.matches("path/to/something/else"));
|
||||
|
||||
// Test regex pattern
|
||||
let regex = TrapPattern::as_regex(r".*eval-stdin\.php.*");
|
||||
assert!(regex.matches("/vendor/phpunit/phpunit/src/Util/PHP/eval-stdin.php"));
|
||||
assert!(regex.matches("/tests/eval-stdin.php?param"));
|
||||
assert!(!regex.matches("/normal/path"));
|
||||
|
||||
// Test invalid regex pattern (should return false)
|
||||
let invalid = TrapPattern::Regex {
|
||||
pattern: "(invalid[regex".to_string(),
|
||||
regex: true,
|
||||
};
|
||||
assert!(!invalid.matches("anything"));
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn test_should_tarpit() {
|
||||
let config = Config::default();
|
||||
|
@ -1376,6 +1512,25 @@ mod tests {
|
|||
)
|
||||
.await
|
||||
);
|
||||
|
||||
// Test regex patterns
|
||||
assert!(
|
||||
should_tarpit(
|
||||
"/index.php?s=/index/\\think\\app/invokefunction&function=call_user_func_array&vars[0]=md5&vars[1][]=Hello",
|
||||
&IpAddr::V4(Ipv4Addr::new(1, 2, 3, 4)),
|
||||
&config
|
||||
)
|
||||
.await
|
||||
);
|
||||
|
||||
assert!(
|
||||
should_tarpit(
|
||||
"/hello.world?%ADd+allow_url_include%3d1+%ADd+auto_prepend_file%3dphp://input",
|
||||
&IpAddr::V4(Ipv4Addr::new(1, 2, 3, 4)),
|
||||
&config
|
||||
)
|
||||
.await
|
||||
);
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
|
@ -1403,6 +1558,7 @@ mod tests {
|
|||
state.blocked.insert(ip1);
|
||||
assert!(state.blocked.contains(&ip1));
|
||||
assert!(!state.blocked.contains(&ip2));
|
||||
drop(state);
|
||||
}
|
||||
|
||||
// Test active connections
|
||||
|
@ -1416,6 +1572,7 @@ mod tests {
|
|||
assert_eq!(state.active_connections.len(), 1);
|
||||
assert!(!state.active_connections.contains(&ip1));
|
||||
assert!(state.active_connections.contains(&ip2));
|
||||
drop(state);
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -1477,4 +1634,30 @@ mod tests {
|
|||
assert_eq!(choose_response_type("/api/v1/users"), "api");
|
||||
assert_eq!(choose_response_type("/index.html"), "generic");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_config_file_formats() {
|
||||
// Create temporary JSON config file
|
||||
let temp_dir = std::env::temp_dir();
|
||||
let json_path = temp_dir.join("temp_config.json");
|
||||
let toml_path = temp_dir.join("temp_config.toml");
|
||||
|
||||
let config = Config::default();
|
||||
|
||||
// Test JSON serialization and deserialization
|
||||
config.save_to_file(&json_path).unwrap();
|
||||
let loaded_json = Config::load_from_file(&json_path).unwrap();
|
||||
assert_eq!(loaded_json.listen_addr, config.listen_addr);
|
||||
assert_eq!(loaded_json.min_delay, config.min_delay);
|
||||
|
||||
// Test TOML serialization and deserialization
|
||||
config.save_to_file(&toml_path).unwrap();
|
||||
let loaded_toml = Config::load_from_file(&toml_path).unwrap();
|
||||
assert_eq!(loaded_toml.listen_addr, config.listen_addr);
|
||||
assert_eq!(loaded_toml.min_delay, config.min_delay);
|
||||
|
||||
// Clean up
|
||||
let _ = std::fs::remove_file(json_path);
|
||||
let _ = std::fs::remove_file(toml_path);
|
||||
}
|
||||
}
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue