一次真实的代码审计:连安全扫描器都扫不到的老系统配置长什么样?

38 阅读10分钟

老炮踩坑录 · 翻车现场系列

基于真实企业内网系统的配置实读,连接串与口令逐条附行号

关键词:properties 注释 · 明文口令 · 密文复用 · FTP 账号 · 配置复制

👋 欢迎阅读

属性配置密码-csdn-fm.png 🏠个人主页:知守观
📘我的专栏: 老炮踩坑录
💻当前内容:配置里的连接串与口令

引言

上一篇写这个仓库的两代 Web 框架,翻 pom 翻出来的。这篇翻的是配置。

现在做密钥扫描的平台,默认盯着 application.yml、application-prod.yml,盯着 .env,盯着 Java 里的常量字符串。这套系统的技术栈太老,配置不用 yml,用的是 properties;连接信息不用框架的约定键名,用的是 jdbc.url、defaultDataSource.jdbcUrl 这种自定义键。再加上注释行,扫描规则一条都套不上。

我在这个仓库的 resources/config 目录下,翻出四种不同性质的口令暴露方式。注释里的明文 Oracle 账号、可以跨库复用的密文、FTP 地址里的账号,还有同一份配置在四个模块里各存一份。

第一个文件:注释里的 Oracle 账号,是真实在用的那种

portal-web/resources/config/datasource.properties 开头六行:

#oracle version database settings
#jdbc.driver=oracle.jdbc.driver.OracleDriver
#jdbc.url=jdbc:oracle:thin:@10.20.30.41:1521:proddb
#jdbc.username=appuser
#jdbc.password=pwd01

Oracle 全套连接信息,driver、url、username、password 四行齐整,全部以 # 开头。注释掉了,当前不生效。

紧跟着第 8 到 12 行是实际生效的:

#jdbc.driver=org.apache.derby.jdbc.ClientDriver
jdbc.driver=com.p6spy.engine.spy.P6SpyDriver
jdbc.url=jdbc:derby://10.20.30.52:1527:appdb
jdbc.username=app
jdbc.password=app

这一段能读出三件事。

切换数据源的动作是改注释。Oracle 那套留着不删,Derby 那套注释掉一行再启用,这就是这个项目的"多环境配置"。项目里没有 profile 概念,没有 -Dspring.profiles.active,运维要换库就上去改 properties 再重启。

生效的驱动写的是 p6spy。com.p6spy.engine.spy.P6SpyDriver 是个 SQL 监听驱动,挂在这个位置意味着所有 SQL 都过它一遍。p6spy 的具体输出位置要看 spy.properties,这个文件我在配置目录里没找到,输出到哪里我没核实。生产环境挂着 SQL 监听驱动这件事本身,就够单独说一遍。

生效的账号是 app/app。用户名和密码一样,这个习惯在后面还有更夸张的例子。

连接池参数也在同一个文件里,第 14 到 19 行:

c3p0.minPoolSize=1
c3p0.maxPoolSize=2
c3p0.initialPoolSize=2
c3p0.maxIdleTime=30000
c3p0.checkoutTimeout=10000

最大连接数 2。这类后台系统并发不高,这个数字可能是照着实际压测调出来的,也可能是没人动过。拿不到当时的容量数据,我不猜原因,只把数字摆出来——一个跑在生产上的服务,连接池上限是 2。

第二个文件:RAC 的完整描述符,也在注释里

client.properties 第 41 到 53 行:

#Default data source configuration for the client
defaultDataSource.driverClass = oracle.jdbc.driver.OracleDriver
########### For HA DB ############
#defaultDataSource.jdbcUrl = jdbc:oracle:thin:@10.20.30.41:1521:proddb
########### For RAC DB ############
# defaultDataSource.jdbcUrl = jdbc:oracle:oci:@(DESCRIPTION=(ADDRESS_LIST=(ADDRESS=(PROTOCOL=TCP)(HOST=10.20.30.61)(PORT=1521))(ADDRESS=(PROTOCOL=TCP)(HOST=10.20.30.62)(PORT=1521))(load_balance=no)(failover=yes))(CONNECT_DATA=(SERVICE_NAME=svc1)(failover_mode=(type=select)(method=basic)(retries=5)(delay=100))))
#defaultDataSource.user = Bx4nVc9jTa=
#defaultDataSource.password = Qm7tRk2pLw=
defaultDataSource.minPoolSize = 2
defaultDataSource.maxPoolSize = 5
defaultDataSource.initialPoolSize = 2
defaultDataSource.maxIdleTime = 3600
defaultDataSource.checkoutTimeout = 10000

两种数据库部署形态写在一起:单机 HA 一套,RAC 一套,靠注释切换。RAC 那条是完整的 TNS 描述符,两个节点 IP、服务名、failover 参数(retries=5, delay=100)全在里面。

这份配置比上一份危险的地方在于,它把数据库的拓扑结构也一起交出去了。拿到这段的人,知道这是双节点 RAC、服务名是什么、故障切换重试 5 次。做渗透的时候,这类信息比单个 IP 值钱。

账号密码这里不是明文,是 Bx4nVc9jTa= 和 Qm7tRk2pLw= 这种形式。Base64 编码的 8 字节块,对应的是一套对称加密。同一目录下的第 58 到 59 行给了线索:

# Default password encypt policy 0: MD5  1: SHA256
servparam.defaultencrypttype = 1

加密策略默认值写死成 SHA256。源码里 encypt 这个词也拼错了,跟后面的 dom't 一个路数。

同一个密文,出现在五个地方

Qm7tRk2pLw= 这个串,我在四个模块的配置文件里都见到了,而且都在各自文件的第 48 行:

alarm-web/resources/config/client.properties:48
portal-web/resources/config/client.properties:48
sms-web/resources/config/client.properties:48
number-web/resources/config/client.properties:48

同一份文件复制四份这件事后面单说。这里先看另一处更值得看的地方,sms-web/resources/config/alarmclient.properties 第 26 到 29 行:

# database information of Kpi
kpidb.url=jdbc:oracle:thin:@10.20.40.11:1521:orcl
kpidb.username=Ht3wPz6yEd=
kpidb.password=Qm7tRk2pLw=

同样的 Qm7tRk2pLw=,这次是不带注释、真正生效的数据库口令。也就是说,一个监控库和另一套库里被注释掉的那条连接,用的是同一个口令。

再看第 41 到 44 行:

# "|" is reserved for one key word in our system, please dom't use "|" in any account and password 
alm.db.url=jdbc:oracle:thin:@10.20.40.11:1521:orcl|jdbc:oracle:thin:@10.20.40.11:1521:orcl
alm.db.username=Ht3wPz6yEd=|Bx4nVc9jTa=
alm.db.password=Qm7tRk2pLw=|Qm7tRk2pLw=

这里用 | 分隔了两套账号,一主一备。两边列的密码密文一模一样,两个不同账号共用同一个口令。

这段密文的加密方式从行为上能反推:同一个明文密码,在四个模块、三套账号、多份配置里,加密出来是同一个字符串。说明加密过程没有随机量参与,同样的输入永远得同样的输出。这类加密的弱点很明确——密文可以被当作口令来比对和复用。拿到 Qm7tRk2pLw= 的人不需要知道原文,只要目标系统的验证逻辑接受这个密文,就能进;如果验证逻辑拿密文去比密文,连解密都不用。

第 41 行那句注释是另一层信息。"|" is reserved for one key word in our system, please dom't use "|" in any account and password——系统内部用竖线做账号分隔符,所以口令里不许出现竖线。这句注释把口令的字符集限制也交代了。

一个 FTP 地址,账号和密码都在里面

alarmclient.properties 第 18 行:

filepath=ftp://ftpsvc:ftpsvc@10.20.30.66/share/proxy/test.txt|ftp://jack:jack@10.20.30.66/share/proxy/test.txt

两个 FTP 账号,ftpsvc/ftpsvc 和 jack/jack,用户名和密码完全一样,写在 URL 里。这行是文件服务器健康检查用的,配置里叫"监测文件服务器"。

FTP 的账号密码写进 URL,好处是简单,java.net.URL 直接就能连。代价是这行配置在四份复制品里各存一遍,日志、备份、工单附件里都可能有它。用户名等于密码这种事,用字典猜一遍就中。

同一个目录下的 grclient.properties 和 gr.properties,第 5 到 7 行:

# datastation server user
datastation.server.user = admin
# datastation server password
datastation.server.password = 3f9a1c7d24b8e05a6f13c9d7e20b4a8c56d1e9fb

账号 admin,密码是一串 40 位十六进制。40 位正好是 SHA-1 摘要的十六进制长度,我没有找到这个值的使用代码,无法确认它到底是 SHA-1 摘要还是某种自定义编码,不下结论。能确定的是它没有加盐的痕迹,同一个值在任何一份配置里都一致。

同一份配置,四个模块各存一份

datasource.properties 和 client.properties 这两个文件名,在下面四个模块里各出现一次:

alarm-web/resources/config/
portal-web/resources/config/
sms-web/resources/config/
number-web/resources/config/

我逐行比对过 datasource.properties,四份内容一致,包括注释掉的 Oracle 连接串和生效的 app/app。client.properties 第 48 行那个密文,四份也一样。

这意味着改一次数据库密码,要动四个模块的配置文件,重新打四个包。漏掉一个,那个模块连不上库,排查的时候还得先想明白"是网络问题还是有人漏改配置"。反过来,多存一份就多一个泄露点,仓库里任何一处被翻出去,口令就出去了。

配置本身没毛病,扫描工具够不着它才是毛病

这段我特意说清楚:这些配置文件的写法,在 2010 年那个语境下不算错。properties 是当年 Web 应用的标准配置方式,注释切换多环境是当时的常见做法,对称加密存口令在当时也是"做了安全处理"的正面动作。

问题在于十年之后,安全扫描的默认规则已经收敛到几个固定模式:yml 文件里的 password:、Java 里的 "secret"、.env。这套系统的配置落在规则之外。注释行更是在所有扫描规则的盲区里——扫描器通常只匹配未注释的有效配置,注释掉的行按噪音处理。

所以这套仓库交给依赖扫描平台跑,app/app 和 appuser/pwd01 这两组口令大概率一条告警都不会出。

对照表

image.png

有两处我没确认:一是 p6spy 的 spy.properties 没找到,SQL 日志最终落在磁盘还是别处,我说不清;二是那串 40 位十六进制口令的使用代码没追到,SHA-1 只是按长度推测,不能当结论。用过这套系统的同行,评论区聊聊。

自查打卡:

跑一句: git grep -nE "^\s*#" -- "*.properties" | grep -iE "password|passwd|pwd|jdbc:" 这条命令专门找注释掉的连接信息。打在评论区,看看谁的配置文件里全是“被盗的密码”。。

老炮点评

这套配置里最该被重视的,是 Qm7tRk2pLw= 在五个地方重复出现。

明文口令的问题是显眼的,团队里谁都看得出 app/app 不妥。加密之后,从心理上这个问题就被结掉了——"已经加密了"。实际上这套加密出来的结果可复用、可跨库、可跨模块,拿到一个等于拿到全部。加密只改变了口令的存储形式,没有改变口令的复用结构。

只用一种做法就够用:把口令从仓库里全部拿出去,配置里只保留键名和占位符,值由部署时的环境变量或者配置中心注入。这件事在任何一代技术栈上都做得到,${DB_PASSWORD} 这种写法主流框架早十年前就支持,properties 文件也能用 System Property 覆盖。

对这套系统的现实建议,我按代价排了个顺序:FTP 那两行和 app/app 排第一,这四个值是直接可用的凭据,改配置加改密码,一天能完;四个模块的 config 目录合并成一份共用配置排第二,主要目的是减少重复;密文那部分要换加密方案就得连带改验证逻辑,量级按周算,老系统动它的收益和风险都得重新算一遍,放在后面做。

最后问一句

你维护的仓库里,有多少口令是"加密过的"?评论区说说你见过的加密方式,我这边见过最离谱的是 Base64 之后又反转一次字符串。

如果本文对你有帮助,欢迎:

👍 点赞 | ⭐ 收藏 | 👤 关注 | 💬 留言

欢迎留言探讨。你的每一次互动,都是我继续更新的动力🚀


下期预告:《iBATIS 2.3.4 考古:MyBatis 改名前的持久层长什么样》

还是这个仓库。两个老模块的数据访问层用的是 ibatis-sqlmap 2.3.4.726,没有 Mapper 接口,没有注解,SQL 全在 xml 的 sqlmap 里,代码里靠 SqlMapClient 点名调用。这套写法读起来什么样,跟今天的 MyBatis 差在哪,下一篇把 sqlmap 配置和调用代码拆开看。

我是老炮,18 年 Java 老兵,仍在一线。关注「Java老炮踩坑录」,真实项目复盘,让你少走弯路。