Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Binary file not shown.
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
#!/usr/bin/env escript
%%! -sname ssh_runner -noinput

main([PortStr]) ->
Port = list_to_integer(PortStr),

io:format("Starting Erlang SSH daemon on port ~p~n", [Port]),

case application:ensure_all_started(ssh) of
{ok, _StartedApps} ->
ok;
{error, StartReason} ->
io:format("Failed to start ssh application dependencies: ~p~n", [StartReason]),
halt(1)
end,

KeyDir = "/opt/erlang_ssh/ssh_keys",

case ssh:daemon(Port, [
{system_dir, KeyDir},
{idle_time, infinity}
]) of
{ok, Pid} ->
io:format("SSH daemon started successfully on port ~p, pid: ~p~n", [Port, Pid]),
receive
stop -> ok
end;
{error, DaemonReason} ->
io:format("Failed to start SSH daemon: ~p~n", [DaemonReason]),
halt(1)
end.
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
class ssh_erlangotp_rce::config {
$secgen_parameters = secgen_functions::get_parameters($::base64_inputs_file)
$leaked_filenames = $secgen_parameters['leaked_filenames']
$strings_to_leak = $secgen_parameters['strings_to_leak']
$ssh_username = $secgen_parameters['unix_username'][0]
$ssh_home = "/home/${ssh_username}"

file { '/opt/erlang_ssh':
ensure => directory,
owner => 'root',
group => 'root',
mode => '0755',
}

file { '/opt/erlang_ssh/ssh_keys':
ensure => directory,
owner => $ssh_username,
group => $ssh_username,
mode => '0700',
}

file { '/opt/erlang_ssh/start_ssh.escript':
ensure => present,
owner => 'root',
group => 'root',
mode => '0755',
source => 'puppet:///modules/ssh_erlangotp_rce/start_ssh.escript',
require => File['/opt/erlang_ssh'],
}

::secgen_functions::leak_files { 'erlang_ssh-flag':
storage_directory => $ssh_home,
leaked_filenames => $leaked_filenames,
strings_to_leak => $strings_to_leak,
owner => $ssh_username,
mode => '0600',
leaked_from => 'ssh_erlangotp_rce',
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,53 @@
class ssh_erlangotp_rce::install {
$secgen_parameters = secgen_functions::get_parameters($::base64_inputs_file)
$ssh_username = $secgen_parameters['unix_username'][0]
$ssh_home = "/home/${ssh_username}"
$status_log = '/var/log/ssh_erlangotp_rce/stage_status.log'

ensure_packages([
'openssh-client',
])

file { '/var/log/ssh_erlangotp_rce':
ensure => directory,
owner => 'root',
group => 'root',
mode => '0755',
}

file { '/tmp/erlang-otp-prebuilt_26.2.5.10-1_amd64.deb':
ensure => file,
source => 'puppet:///modules/ssh_erlangotp_rce/erlang-otp-prebuilt_26.2.5.10-1_amd64.deb',
mode => '0644',
}

exec { 'install-erlang-otp-prebuilt':
command => "/bin/sh -c 'echo \"$(date -Is) install:start\" >> ${status_log}; if /usr/bin/dpkg -i /tmp/erlang-otp-prebuilt_26.2.5.10-1_amd64.deb > /var/log/ssh_erlangotp_rce/install.log 2>&1; then echo \"$(date -Is) install:ok\" >> ${status_log}; touch /var/log/ssh_erlangotp_rce/.install_ok; else rc=$?; echo \"$(date -Is) install:fail rc=\$rc\" >> ${status_log}; touch /var/log/ssh_erlangotp_rce/.install_fail; tail -n 120 /var/log/ssh_erlangotp_rce/install.log >> ${status_log}; exit \$rc; fi'",
creates => '/usr/local/bin/erl',
path => ['/usr/bin', '/bin', '/usr/sbin', '/sbin'],
logoutput => true,
require => [
File['/var/log/ssh_erlangotp_rce'],
File['/tmp/erlang-otp-prebuilt_26.2.5.10-1_amd64.deb'],
],
}

user { $ssh_username:
ensure => present,
home => $ssh_home,
managehome => true,
shell => '/bin/bash',
}

group { $ssh_username:
ensure => present,
}

file { $status_log:
ensure => file,
owner => 'root',
group => 'root',
mode => '0644',
require => File['/var/log/ssh_erlangotp_rce'],
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,81 @@
class ssh_erlangotp_rce::service {
$secgen_parameters = secgen_functions::get_parameters($::base64_inputs_file)
$ssh_port = pick($secgen_parameters['ssh_port'], ['2222'])
$port = $ssh_port[0]
$ssh_username = $secgen_parameters['unix_username'][0]

exec { 'generate-ssh-host-rsa-key':
command => "/bin/sh -c '/usr/sbin/runuser -u ${ssh_username} -- ssh-keygen -t rsa -b 2048 -f /opt/erlang_ssh/ssh_keys/ssh_host_rsa_key -N \"\" > /var/log/ssh_erlangotp_rce/ssh_keygen_rsa.log 2>&1'",
path => ['/usr/sbin', '/usr/bin', '/sbin', '/bin'],
creates => '/opt/erlang_ssh/ssh_keys/ssh_host_rsa_key',
logoutput => true,
require => [
File['/var/log/ssh_erlangotp_rce'],
File['/opt/erlang_ssh/ssh_keys'],
User[$ssh_username],
],
}

exec { 'generate-ssh-host-ecdsa-key':
command => "/bin/sh -c '/usr/sbin/runuser -u ${ssh_username} -- ssh-keygen -t ecdsa -b 256 -f /opt/erlang_ssh/ssh_keys/ssh_host_ecdsa_key -N \"\" > /var/log/ssh_erlangotp_rce/ssh_keygen_ecdsa.log 2>&1'",
path => ['/usr/sbin', '/usr/bin', '/sbin', '/bin'],
creates => '/opt/erlang_ssh/ssh_keys/ssh_host_ecdsa_key',
logoutput => true,
require => Exec['generate-ssh-host-rsa-key'],
}

file { '/etc/systemd/system/erlang-otp-ssh-rce.service':
ensure => file,
owner => 'root',
group => 'root',
mode => '0644',
content => @("UNIT"),
[Unit]
Description=Erlang OTP vulnerable SSH daemon
After=network-online.target
Wants=network-online.target

[Service]
Type=simple
User=${ssh_username}
Group=${ssh_username}
WorkingDirectory=/opt/erlang_ssh
ExecStart=/usr/local/bin/escript /opt/erlang_ssh/start_ssh.escript ${port}
Restart=on-failure
RestartSec=3
StandardOutput=append:/var/log/ssh_erlangotp_rce/ssh_daemon_start.log
StandardError=append:/var/log/ssh_erlangotp_rce/ssh_daemon_start.log

[Install]
WantedBy=multi-user.target
| UNIT
notify => Exec['systemd-daemon-reload'],
require => [
File['/var/log/ssh_erlangotp_rce'],
Exec['generate-ssh-host-ecdsa-key'],
File['/opt/erlang_ssh/start_ssh.escript'],
Exec['install-erlang-otp-prebuilt'],
User[$ssh_username],
],
}

exec { 'systemd-daemon-reload':
command => '/bin/systemctl daemon-reload',
path => ['/usr/bin', '/bin', '/usr/sbin', '/sbin'],
refreshonly => true,
}

service { 'erlang-otp-ssh-rce':
ensure => running,
enable => true,
provider => 'systemd',
subscribe => [
File['/etc/systemd/system/erlang-otp-ssh-rce.service'],
File['/opt/erlang_ssh/start_ssh.escript'],
],
require => [
Exec['systemd-daemon-reload'],
Exec['generate-ssh-host-ecdsa-key'],
],
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,59 @@
<?xml version="1.0"?>
<vulnerability xmlns="http://www.github/cliffe/SecGen/vulnerability"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://www.github/cliffe/SecGen/vulnerability">
<name>Erlang/OTP SSH Unauthenticated RCE</name>
<author>Rosie Fletcher</author>
<module_license>MIT</module_license>
<description>Erlang/OTP SSH server allows unauthenticated remote code execution via crafted SSH protocol messages. CVE-2025-32433 affects versions prior to OTP-27.3.3, OTP-26.2.5.11, and OTP-25.3.2.20. The vulnerability exists in the SSH handshake message handling where message types >= 80 are incorrectly processed before authentication.</description>

<type>erlang_ssh</type>
<privilege>user_rwx</privilege>
<access>remote</access>
<platform>linux</platform>
<difficulty>low</difficulty>

<read_fact>strings_to_leak</read_fact>
<read_fact>leaked_filenames</read_fact>
<read_fact>unix_username</read_fact>

<default_input into="strings_to_leak">
<generator type="message_generator"/>
</default_input>

<default_input into="leaked_filenames">
<generator type="filename_generator"/>
</default_input>

<default_input into="unix_username">
<generator type="username_generator"/>
</default_input>

<cve>CVE-2025-32433</cve>
<reference>https://github.com/erlang/otp/security/advisories/GHSA-37cp-fgq5-7wc2</reference>
<reference>https://nvd.nist.gov/vuln/detail/CVE-2025-32433</reference>
<software_name>erlang</software_name>
<software_license>Apache-2.0</software_license>

<hint>Erlang/OTP SSH server running on 2222 port</hint>
<solution>Erlang/OTP SSH is vulnerable to pre-authentication RCE. Exploit via metasploit module (linux/ssh/ssh_erlangotp_rce).</solution>

<conflict>
<software_name>erlang</software_name>
</conflict>
<requires>
<type>update</type>
</requires>

<CyBOK KA="MAT" topic="Attacks and exploitation">
<keyword>EXPLOITATION</keyword>
<keyword>EXPLOITATION FRAMEWORKS</keyword>
</CyBOK>
<CyBOK KA="SS" topic="Categories of Vulnerabilities">
<keyword>CVEs and CWEs</keyword>
</CyBOK>
<CyBOK KA="SOIM" topic="PENETRATION TESTING">
<keyword>PENETRATION TESTING - SOFTWARE TOOLS</keyword>
<keyword>PENETRATION TESTING - ACTIVE PENETRATION</keyword>
</CyBOK>
</vulnerability>
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
include ssh_erlangotp_rce::install
include ssh_erlangotp_rce::config
include ssh_erlangotp_rce::service
109 changes: 109 additions & 0 deletions scenarios/ctf/SSH_it_happens.xml
Original file line number Diff line number Diff line change
@@ -0,0 +1,109 @@
<?xml version="1.0"?>
<scenario xmlns="http://www.github/cliffe/SecGen/scenario"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://www.github/cliffe/SecGen/scenario">

<name>SSH-it happens</name>
<author>Rosie Fletcher</author>
<description>
A server is running an Erlang/OTP SSH daemon vulnerable to CVE-2025-32433.
Exploit the pre-authentication RCE to gain root access and capture the flag.
</description>

<type>ctf</type>
<type>attack-ctf</type>
<difficulty>intermediate</difficulty>

<CyBOK KA="WAM" topic="Server-Side Vulnerabilities and Mitigations">
<keyword>server-side misconfiguration and vulnerable components</keyword>
<keyword>Missing Authentication</keyword>
</CyBOK>
<CyBOK KA="MAT" topic="Attacks and exploitation">
<keyword>EXPLOITATION</keyword>
<keyword>EXPLOITATION FRAMEWORKS</keyword>
</CyBOK>
<CyBOK KA="SS" topic="Categories of Vulnerabilities">
<keyword>CVEs and CWEs</keyword>
</CyBOK>
<CyBOK KA="SOIM" topic="PENETRATION TESTING">
<keyword>PENETRATION TESTING - SOFTWARE TOOLS</keyword>
<keyword>PENETRATION TESTING - ACTIVE PENETRATION</keyword>
</CyBOK>

<system>
<system_name>attack_vm</system_name>
<base distro="Kali" name="MSF" />

<input into_datastore="IP_addresses">
<value>172.16.0.2</value>
<value>172.16.0.3</value>
</input>

<utility module_path=".*/parameterised_accounts">
<input into="accounts">
<value>{"username":"kali","password":"kali","super_user":"true","strings_to_leak":[],"leaked_filenames":[]}</value>
</input>
</utility>

<utility module_path=".*/iceweasel">
<input into="accounts">
<value>
{"username":"kali","password":"kali","super_user":"true","strings_to_leak":[],"leaked_filenames":[]}</value>
</input>
<input into="autostart">
<value>false</value>
</input>
</utility>

<utility module_path=".*/kali_web" />
<utility module_path=".*/metasploit_framework" />
<utility module_path=".*/nmap" />
<utility module_path=".*/handy_cli_tools" />

<network type="private_network">
<input into="IP_address">
<datastore access="0">IP_addresses</datastore>
</input>
</network>
<input into_datastore="spoiler_admin_pass">
<generator type="strong_password_generator"/>
</input>
<build type="cleanup">
<input into="root_password">
<datastore>spoiler_admin_pass</datastore>
</input>
</build>
</system>

<system>
<system_name>server</system_name>
<base distro="Debian 12" type="desktop" name="KDE" />

<vulnerability module_path=".*/ssh_erlangotp_rce">
<input into="strings_to_leak">
<generator type="flag_generator" />
</input>
<input into="ssh_port">
<value>2222</value>
</input>
</vulnerability>

<vulnerability module_path=".*/sudo_root_awk|.*/sudo_root_less">
<input into="strings_to_leak">
<generator type="flag_generator"/>
</input>
</vulnerability>

<network type="private_network">
<input into="IP_address">
<datastore access="1">IP_addresses</datastore>
</input>
</network>
<build type="cleanup">
<input into="root_password">
<datastore>spoiler_admin_pass</datastore>
</input>
</build>
</system>

</scenario>