use crate::app::mods::inspector::code_view_ui;
use base64::{engine::general_purpose, Engine as _};
use openssl::encrypt::Decrypter;
use openssl::hash::MessageDigest;
use openssl::pkey::PKey;
use openssl::rsa::{Padding, Rsa};
use openssl::symm::{decrypt, Cipher};
use poll_promise::Promise;
use rand::{distributions::Alphanumeric, Rng};
use reqwest::{blocking::Client, header};
use serde_json;
use std::collections::HashMap;
use std::fmt;
use uuid::Uuid;
type GenErr = Box<dyn std::error::Error>;
#[derive(serde::Deserialize, serde::Serialize)]
#[serde(default)]
pub struct Interact {
#[serde(skip)]
rsa: Rsa<openssl::pkey::Private>,
secret: String,
correlation_id: String,
#[serde(skip)]
client: Client,
domain: Option<String>,
is_active: bool,
is_minimized: bool,
output: Vec<InteractResponse>,
#[serde(skip)]
pub promise: Option<Promise<Result<Vec<String>, reqwest::Error>>>,
}
impl Default for Interact {
fn default() -> Self {
Self::new()
}
}
impl fmt::Display for Interact {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
let pubk = self.pubk();
let privk = self.privk();
write!(f, "{}\n{}", privk, pubk)
}
}
impl fmt::Debug for Interact {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(f, "{}", self)
}
}
#[derive(serde::Deserialize, serde::Serialize)]
pub enum InteractResponse {
Http(String, String, String, String, String),
Dns(String, String, String, String, String, String),
Other(String, String),
}
impl InteractResponse {
pub fn from_str(s: &str) -> Self {
let obj: Result<serde_json::Value, _> = serde_json::from_str(s);
match obj {
Ok(json_obj) => {
let proto = json_obj["protocol"].to_string();
match proto.trim_matches('"') {
"http" => Self::Http(
json_obj["full-id"]
.to_string()
.trim_matches('"')
.to_string(),
json_obj["raw-request"]
.to_string()
.trim_matches('"')
.to_string(),
json_obj["raw-response"]
.to_string()
.trim_matches('"')
.to_string(),
json_obj["remote-addr"]
.to_string()
.trim_matches('"')
.to_string(),
json_obj["timestamp"]
.to_string()
.trim_matches('"')
.to_string(),
),
"dns" => {
let ts = json_obj["timestamp"]
.to_string()
.trim_matches('"')
.to_string();
Self::Dns(
format!(
"{}: {}",
json_obj["unique-id"].to_string().trim_matches('"'),
ts
),
json_obj["q-type"].to_string().trim_matches('"').to_string(),
json_obj["raw-request"]
.to_string()
.trim_matches('"')
.to_string(),
json_obj["raw-response"]
.to_string()
.trim_matches('"')
.to_string(),
json_obj["remote-addr"]
.to_string()
.trim_matches('"')
.to_string(),
ts.to_string(),
)
}
_ => Self::Other(format!("{:?}", json_obj), s.to_string()),
}
}
Err(e) => Self::Other(e.to_string(), s.to_string()),
}
}
}
impl Interact {
pub fn new() -> Self {
let rsa = Rsa::generate(2048).unwrap();
let secret = format!("{}", Uuid::new_v4());
let u4_corr = format!("{}", Uuid::new_v4());
let correlation_id = u4_corr.replace('-', "")[..20].to_string();
let mut headers = header::HeaderMap::new();
headers.insert(
"Content-Type",
header::HeaderValue::from_static("application/json"),
);
let client = Client::builder()
.default_headers(headers)
.danger_accept_invalid_certs(true)
.build()
.unwrap();
let domain = None;
let is_active = true;
let is_minimized = false;
let promise = None;
let output = vec![];
Self {
rsa,
secret,
correlation_id,
client,
domain,
is_active,
is_minimized,
promise,
output,
}
}
fn pubk(&self) -> String {
String::from_utf8_lossy(&self.rsa.public_key_to_pem().unwrap()).to_string()
}
fn privk(&self) -> String {
String::from_utf8_lossy(&self.rsa.private_key_to_pem().unwrap()).to_string()
}
pub fn pkey(&self) -> Result<PKey<openssl::pkey::Private>, GenErr> {
let rsa = self.rsa.clone();
PKey::from_rsa(rsa).or(Err("Could not build PKey from rsa".into()))
}
pub fn pubk_u8(&self) -> Vec<u8> {
self.rsa.public_key_to_pem().unwrap().to_vec()
}
pub fn correlation_id(&self) -> &str {
&self.correlation_id
}
pub fn secret(&self) -> &str {
&self.secret
}
pub fn gen_domain(&mut self) {
let random: String = rand::thread_rng()
.sample_iter(&Alphanumeric)
.take(13)
.map(char::from)
.collect();
let site = format!("{}{}", self.correlation_id(), random);
self.domain = Some(format!("{}.oast.site", site));
}
pub fn domain(&self) -> Option<String> {
self.domain.clone()
}
pub fn output(&self) -> &Vec<InteractResponse> {
&self.output
}
pub fn output_mut(&mut self) -> &mut Vec<InteractResponse> {
&mut self.output
}
pub fn poll(
correlation_id: &str,
secret: &str,
pkey: PKey<openssl::pkey::Private>,
) -> Result<Vec<String>, reqwest::Error> {
let url = format!(
"https://oast.site/poll?id={}&secret={}",
correlation_id, secret
);
let resp = reqwest::blocking::get(url)?;
let txt = resp.text()?;
let data: serde_json::Value = serde_json::from_str(&txt).unwrap();
let aes_key_raw = format!("{}", &data["aes_key"])
.trim_matches('"')
.to_string();
let data_arr = &data["data"];
let mut decrypter = Decrypter::new(&pkey).unwrap();
decrypter.set_rsa_padding(Padding::PKCS1_OAEP).unwrap();
decrypter.set_rsa_oaep_md(MessageDigest::sha256()).unwrap();
if let Some(arr) = data_arr.as_array() {
let key_decoded = general_purpose::STANDARD.decode(aes_key_raw).unwrap();
let buffer_len = decrypter.decrypt_len(&key_decoded).unwrap();
let mut aes_key = vec![0; buffer_len];
let decrypted_len = decrypter.decrypt(&key_decoded, &mut aes_key).unwrap();
aes_key.truncate(decrypted_len);
let mut results = vec![];
for cipher in arr {
let cipher = format!("{}", cipher).trim_matches('"').to_string();
let cipher_decoded = general_purpose::STANDARD.decode(cipher).unwrap();
let decrypted = Self::aes_decrypt(&aes_key, &cipher_decoded).unwrap();
let parsed = String::from_utf8_lossy(&decrypted).to_string();
results.push(parsed);
}
Ok(results)
} else {
Ok(vec![])
}
}
fn aes_decrypt(key: &[u8], cipher: &[u8]) -> Result<Vec<u8>, GenErr> {
let decryptor = Cipher::aes_256_cfb128();
let iv = &cipher[..16];
decrypt(decryptor, key, Some(iv), &cipher[16..]).or(Err("Could not decrypt".into()))
}
pub fn _unregister(&self) -> Result<(), GenErr> {
let data = HashMap::from([
("secret-key", self.secret().to_string()),
("correlation-id", self.correlation_id().to_string()),
]);
self.client
.post("https://oast.site/deregister")
.body(format!("{:?}", data))
.send()?;
Ok(())
}
pub fn is_minimized(&self) -> bool {
self.is_minimized
}
pub fn is_active(&self) -> bool {
self.is_active
}
pub fn toggle_minimized(&mut self) {
self.is_minimized = !self.is_minimized;
}
pub fn toggle_active(&mut self) {
self.is_active = !self.is_active;
}
pub fn is_registered(&self) -> bool {
self.domain.is_some()
}
pub fn register(
pk: &Vec<u8>,
secret: &str,
correlation_id: &str,
) -> Result<Vec<String>, reqwest::Error> {
let pubk_encoded = general_purpose::STANDARD.encode(pk);
let data = HashMap::from([
("public-key", pubk_encoded),
("secret-key", secret.to_string()),
("correlation-id", correlation_id.to_string()),
]);
let mut headers = header::HeaderMap::new();
headers.insert(
"Content-Type",
header::HeaderValue::from_static("application/json"),
);
let client = Client::builder()
.danger_accept_invalid_certs(true)
.default_headers(headers)
.build()?;
let _resp = client
.post("https://oast.site/register")
.body(format!("{:?}", data))
.send()?;
Ok(vec![])
}
pub fn display(&mut self, ui: &mut egui::Ui) {
if !self.is_registered() {
let pubk_u8 = self.pubk_u8().clone();
let secret = self.secret().to_string();
let correlation_id = self.correlation_id().to_string();
self.promise.get_or_insert_with(|| {
Promise::spawn_thread("int_reg", move || {
Interact::register(&pubk_u8, &secret, &correlation_id)
})
});
}
if let Some(p) = &self.promise {
if let Some(Ok(v)) = p.ready() {
if self.is_registered() {
for s in v.clone().iter() {
let obj = InteractResponse::from_str(s);
self.output_mut().push(obj);
}
} else {
self.gen_domain();
}
self.promise = None;
ui.ctx().request_repaint();
}
}
egui::menu::bar(ui, |ui| {
ui.with_layout(egui::Layout::top_down(egui::Align::RIGHT), |ui| {
ui.horizontal(|ui| {
if ui.button("x").clicked() {
self.toggle_active();
ui.ctx().request_repaint();
}
ui.separator();
let bt = if self.is_minimized() { "+" } else { "-" };
if ui.button(bt).clicked() {
self.toggle_minimized();
ui.ctx().request_repaint();
}
ui.separator();
ui.with_layout(egui::Layout::top_down(egui::Align::Center), |ui| {
ui.label(format!(
"Interact #{}",
self.domain().unwrap_or("NYI".to_string())
));
});
});
});
});
ui.separator();
if self.is_registered() && !self.is_minimized() {
egui::menu::bar(ui, |ui| {
if ui.button("☰ Copy URL").clicked() && self.domain().is_some() {
ui.output_mut(|o| o.copied_text = self.domain().unwrap().to_string());
}
ui.separator();
if ui.button("☰ Refresh").clicked() {
let secret = self.secret().to_string();
let correlation_id = self.correlation_id().to_string();
let pkey = self.pkey().unwrap();
self.promise.get_or_insert_with(|| {
Promise::spawn_thread("int_poll", move || {
Interact::poll(&correlation_id, &secret, pkey)
})
});
}
ui.separator();
});
ui.separator();
egui::ScrollArea::both()
.id_source("bot_interact")
.show(ui, |ui| {
ui.collapsing(format!("results ({})", self.output().len()), |ui| {
let mut http = vec![];
let mut dns = vec![];
let mut other = vec![];
for resp in self.output() {
match resp {
InteractResponse::Http(id, req, res, rema, time) => http.push((
id,
req.replace("\\r\\n", "\r\n"),
res.replace("\\r\\n", "\r\n"),
rema,
time,
)),
InteractResponse::Dns(id, qtype, req, res, rema, time) => {
dns.push((
id,
qtype,
req.replace("\\n", "\n").replace("\\t", "\t"),
res.replace("\\n", "\n").replace("\\t", "\t"),
rema,
time,
))
}
InteractResponse::Other(error, s) => other.push((error, s)),
}
}
ui.collapsing(format!("http ({})", http.len()), |ui| {
for (id, req, res, _rema, time) in http {
ui.push_id(time, |ui| {
ui.collapsing(id.to_string(), |ui| {
ui.label(time.to_string());
ui.collapsing("request", |ui| {
code_view_ui(ui, &req);
});
ui.collapsing("response", |ui| {
code_view_ui(ui, &res);
});
});
});
ui.separator();
}
});
ui.collapsing(format!("dns ({})", dns.len()), |ui| {
for (id, qtype, req, res, _rema, time) in dns {
ui.collapsing(id.to_string(), |ui| {
ui.label(format!("{} - {}", time, qtype));
ui.collapsing("request", |ui| {
code_view_ui(ui, &req);
});
ui.collapsing("response", |ui| {
code_view_ui(ui, &res);
});
});
ui.separator();
}
});
ui.collapsing(format!("other ({})", other.len()), |ui| {
for (e, s) in other {
ui.horizontal(|ui| {
ui.label(format!("error: {} - data: {}", e, s));
});
ui.separator();
}
});
});
});
}
}
}