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