Skip to main content

Documentation Index

Fetch the complete documentation index at: https://vulhunt-docs.binarly.io/llms.txt

Use this file to discover all available pages before exploring further.

This use case demonstrates how to use the functions scope to detect an authentication bypass in telnetd from GNU Inetutils. In telnetd, a _var_short_name function reads and returns the content of the USER environment variable without sanitizing it, as can be seen in the code below:
inetutils/telnetd/utility.c
char *
_var_short_name (struct line_expander *exp)
{
  char *q;
  char timebuf[64];
  time_t t;

  switch (*exp->cp++)
    {
    // ...
    case 'U':
      return getenv ("USER") ? xstrdup (getenv ("USER")) : xstrdup ("");

    default:
      exp->state = EXP_STATE_ERROR;
      return NULL;
    }
}
In this use case, it is assumed that _var_short_name is always inlined into _expand_var. This is normal if a function is called just once and from a single place, which is the case.

Finding _expand_var

To start the rule in the right context, the functions scope can be used like this:
scopes = scope:functions{
  target = "_expand_var",
  with = check
}
This will find the _expand_var function in the binary. As the function name is fixed in this case, there’s no need to use target = {matching = "<regex>"}. Because scope:functions was used, the check function receives two parameters, of types ProjectHandle and FunctionContext, in that order. The first gives access to project-level methods, while the second holds the context of the matched function — _expand_var in this case — and provides access to function-level methods.

Finding getenv

A good rule should annotate relevant addresses precisely. To find the call to getenv, one possible check function is as follows:
function check(project, context)
  -- In the source code there are two calls to `getenv`, but the compiler may
  -- optimize this to a single call. To cover both cases, `context:calls("imp.getenv")`
  -- was used to return ALL calls to `getenv` from the context.
  --
  -- When searching for an `imp.`-prefixed function name, VulHunt automatically
  -- searches for the unprefixed name if `imp.gentev` is not found, thus for
  -- simple cases like this, regular expressions are not needed.
  local getenv_calls = context:calls("imp.getenv")

  -- Arrays in Lua starts at 1, so this line gets the first call
  -- to `getenv` recovered by VulHunt
  local getenv = getenv_calls[1]

  -- If there are two calls to `getenv`, the rule needs to find the second as
  -- the first is only used in the condition check from the source code.
  --
  -- For performance reasons, VulHunt does not guarantee the order of the
  -- returned functions. Therefore, `context:precedes` is used to check
  -- whether the first returned function precedes the second.
  if #getenv_calls == 2 and context:precedes(getenv_calls[1], getenv_calls[2]) then
    getenv = getenv_calls[2]
  end
  -- Keep working with `getenv`
end

Finding start_login and its call to execv

After reading the contents of USER, a start_login function passes it to login(1) (by default), which has a -f <username> option to skip login authentication for a given <username>. Consequently, if an attacker sets USER to -f root, the authentication is bypassed and the attacker will be logged as root. The relevant code is as follows:
inetutils/telnetd/pty.c
void
start_login (char *host, int autologin, char *name)
{
  char *cmd;
  int argc;
  char **argv;
  // --snip--

  // `expand_line` indirectly calls `_expand_var`
  cmd = expand_line (login_invocation);
  if (!cmd)
    fatal (net, "can't expand login command line");
  argcv_get (cmd, "", &argc, &argv);
  execv (argv[0], argv);
  syslog (LOG_ERR, "%s: %m\n", cmd);
  fatalperror (net, cmd);
}
Because the rule is at the function level of _expand_var, it’s necessary to look at the project level to find start_login in the code. The following code uses a method from projectcheck’s first parameter — to achieve it:
function check(project, context)
  -- Previous code to find `getenv`
  local getenv_calls = context:calls("imp.getenv")
  local getenv = getenv_calls[1]

  if #getenv_calls == 2 and context:precedes(getenv_calls[1], getenv_calls[2]) then
    getenv = getenv_calls[2]
  end

  -- New code to find `start_login`
  local start_login = project:functions("start_login")

  if not start_login then
    return
  end

  -- At this point, `start_login` has type `FunctionContext`, just like
  -- the `context` parameter of this `check` function. Therefore, the
  -- same methods used to locate `getenv` can also be used to locate `execv`.

  local execv_calls = start_login:calls("imp.execv")
  -- Keep working with `execv_calls`
end

Returning the evidence

Below is the complete check function to detect this vulnerability. It includes a check for possible calls to strcspn from _expand_var, which are added with the patch. Production-level rules may use a result:patch method to properly inform the binary is patched. Check the Scopes Result Overview in the VulHunt Reference for more information.
telnetd.vh
function check(project, context)
    -- Check for the patch: calls to `strcspn` from `_expand_var`
    if context:has_call("strcspn") then
        print("patched")
        return
    end

    local getenv_calls = context:calls("imp.getenv")
    local getenv = getenv_calls[1]

    if #getenv_calls == 2 and context:precedes(getenv_calls[1], getenv_calls[2]) then
        getenv = getenv_calls[2]
    end

    local start_login = project:functions("start_login")

    if not start_login then return end

    local execv_calls = start_login:calls("imp.execv")

    if not next(execv_calls) then return end

    local execv = execv_calls[1]

    return result:critical{
        name = "CVE-2026-24061",
        description = "In GNU Inetutils 1.9.3 to 2.7, telnetd passes the unsanitized USER environment variable ('-f root') to login, enabling remote authentication bypass",
        evidence = {
            functions = {
                [context.address] = {
                    annotate:prototype "char *_expand_var (struct line_expander *exp)",
                    annotate:at{
                        location = getenv,
                        message = "This call to `getenv` reads the contents of the `USER` environment variable and no sanitization is done afterwards."
                    }
                },
                [start_login.address] = {
                    annotate:prototype "void start_login(char *host, int autologin, char *name)",
                    annotate:at{
                        location = execv,
                        message = "This call to `execv` executes the login(1) program by default with the value previously read from the `USER`\nenvironment variable as its last parameter. A value of \"-f root\" results in an authentication bypass."
                    }
                }
            }
        }
    }
end
VulHunt rules are versatile. Instead of using scope:functions with matching to locate _expand_var, an analyst could target start_login and adapt the remaining logic to identify other functions. There are multiple ways to achieve the same goal.