> ## 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.

# Use Case: Authentication Bypass

This use case demonstrates how to use the [functions scope](/vulhunt-reference/scopes/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:

```c inetutils/telnetd/utility.c theme={null}
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:

```lua theme={null}
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`](/vulhunt-reference/types/project-handle) and [`FunctionContext`](/vulhunt-reference/types/function-context), 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:

```lua theme={null}
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)](https://www.man7.org/linux/man-pages/man1/login.1.html) (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:

```c inetutils/telnetd/pty.c theme={null}
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 `project` -- `check`'s first parameter -- to achieve it:

```lua theme={null}
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](/vulhunt-reference/scopes/scopes-result/overview) in the VulHunt Reference for more information.

```lua telnetd.vh theme={null}
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.
