Stripes MVC框架的对象自动绑定分析

语言: CN / TW / HK

Stripes MVC框架介绍

之前在对某产品的审计过程中发现其使用了一套小众的java mvc框架,联想到今年spring4shell的精彩利用,于是有了这篇文章。今天的主角是Stripes MVC 框架,其诞生的比较早,模仿Struts 的MVC风格,虽然到目前已经停止维护了,但是仍然展示着他的小巧精炼。既然是Struts风格,也就同样具有变量自动绑定的功能,也就是我们所重点关注的功能 – 变量的自动绑定。

基础概念

首先介绍一下整体框架的两个基本概念,ActionBean、Handle。熟悉Struts开发风格的同学应该能大体知道是什么意思。ActionBean类是所有用户自定义Controller的基类,通过实现ActionBean,用户可以创建Controller。通过在ActionBean的实现类中定义Resolution类方法,创建Handler。该handler用于真正处理用户请求。示例代码如下

package corp.qihoo.net.action;

import net.sourceforge.stripes.action.ActionBean;
import net.sourceforge.stripes.action.ActionBeanContext;
import net.sourceforge.stripes.action.DefaultHandler;
import net.sourceforge.stripes.action.ForwardResolution;
import net.sourceforge.stripes.action.Resolution;

public class HelloActionBean implements ActionBean {
    private static final String VIEW = "/WEB-INF/jsp/Hello.jsp";
    private ActionBeanContext context;
    private String firstStr = "";

    public HelloActionBean() {
    }

    public String getFirstStr() {
        return this.firstStr;
    }

    public void setFirstStr(String firstStr) {
        this.firstStr = firstStr;
    }

    public void setContext(ActionBeanContext context) {
        this.context = context;
    }

    public ActionBeanContext getContext() {
        return this.context;
    }

    @DefaultHandler
    public Resolution hello() {
        this.firstStr = "Hello World";
        return new ForwardResolution("/WEB-INF/jsp/Hello.jsp");
    }
}

如上所示,用户创建了HelloActionBean类,并且创建了默认Handler,由此,用户可通过访问 Hello.action 实现默认handler的调用,即返回VIEW常量所定义的view视图。

变量的自动绑定分析

在了解了整个框架的基本使用之后,我们聚焦于变量的自动绑定。首先我们假设访问 Hello.action ,并且提交如下参数 corp.qihoo.net.HelloActionBean.PropertyA=ValueA
框架的整体解析过程如下 net.sourceforge.stripes.controller.DispatcherServlet#service

Resolution resolution = this.requestInit(ctx);
            if (resolution == null) {
                resolution = this.resolveActionBean(ctx);
                if (resolution == null) {
                    resolution = this.resolveHandler(ctx);
                    if (resolution == null) {
                        resolution = this.doBindingAndValidation(ctx);
                        if (resolution == null) {
                            resolution = this.doCustomValidation(ctx);
                            if (resolution == null) {
                                resolution = this.handleValidationErrors(ctx);
                                if (resolution == null) {
                                    resolution = this.invokeEventHandler(ctx);
                                }
                            }
                        }
                    }
                }
            }

resolveActionBean : 根据传递的url,解析ActionBean,

resolveHandler : 根据上面解析的ActionBean, 解析后面要用到的Handle

doBindingAndValidation :这是我们要真正审计的地方,此处实现了参数自动绑定,具体实现为 net.sourceforge.stripes.controller.DefaultActionBeanPropertyBinder#bind(net.sourceforge.stripes.action.ActionBean, net.sourceforge.stripes.action.ActionBeanContext, boolean)

该函数处理流程如下

当传参 corp.qihoo.net.HelloActionBean.PropertyA= ValueA 时,首先遍历传递的参数,依次进行解析,解析过程如下

try {
    eval = new PropertyExpressionEvaluation(PropertyExpression.getExpression(pname), bean);
    } catch (Exception var20) {
        if (pname.equals(context.getEventName())) {
            continue;
        }
        throw var20;
    }

首先根据 corp.qihoo.net.HelloActionBean.PropertyA 获取 PropertyExpressionEvaluation

其中 net.sourceforge.stripes.util.bean.PropertyExpression#getExpression 如下

public static PropertyExpression getExpression(String expression) throws ParseException {
        PropertyExpression parsed = (PropertyExpression)expressions.get(expression);
        if (parsed == null) {
            parsed = new PropertyExpression(expression);
            expressions.put(expression, parsed);
        }

        return parsed;
    }

new PropertyExpression() 时,将对 String expression 进行解析,具体如下

private PropertyExpression(String expression) throws ParseException {
        this.source = expression;
        this.parse(expression);
    }
protected void parse(String expression) throws ParseException {
        ...

        for(int i = 0; i < chars.length; ++i) {
            char ch = chars[i];
            if (escapedChar) {
                builder.append(ch);
                escapedChar = false;
            } else if (ch == '\\') {
                escapedChar = true;
            } else if (!inSingleQuotedString && ch == '\'') {
                inSingleQuotedString = true;
            } else {
                String value;
                if (inSingleQuotedString && ch == '\'') {
                    inSingleQuotedString = false;
                    ...
                    }

                    value = builder.toString();
                    this.addNode(value, value.length() == 1 ? value.charAt(0) : value, inSquareBrackets);
                    builder.setLength(0);
                } else if (inSingleQuotedString) {
                    builder.append(ch);
                } else if (!inDoubleQuotedString && ch == '"') {
                    inDoubleQuotedString = true;
                } else if (inDoubleQuotedString && ch == '"') {
                    inDoubleQuotedString = false;
                   ...
                    }

                    value = builder.toString();
                    this.addNode(value, value, inSquareBrackets);
                    builder.setLength(0);
                } else if (inDoubleQuotedString) {
                    builder.append(ch);
                } else if (!inSquareBrackets && ch == '[') {
                    if (builder.length() > 0) {
                        this.addNode(builder.toString(), (Object)null, inSquareBrackets);
                        builder.setLength(0);
                    }

                    inSquareBrackets = true;
                } else if (inSquareBrackets) {
                    if (ch == ']') {
                        if (builder.length() > 0) {
                            this.addNode(builder.toString(), (Object)null, inSquareBrackets);
                            builder.setLength(0);
                        }

                        inSquareBrackets = false;
                    } else {
                        builder.append(ch);
                    }
                } else if (ch == '.') {
                    if (builder.length() >= 1) {
                        this.addNode(builder.toString(), (Object)null, inSquareBrackets);
                        builder = new StringBuilder();
                    }
                } else {
                    builder.append(ch);
                }
            }

            if (i == chars.length - 1) {
                ...

                if (builder.length() > 0) {
                    this.addNode(builder.toString(), (Object)null, inSquareBrackets);
                }
            }
        }

    }

进而得到一个类似链表的存在,链表中记录了每一个的具体类名和数值类型。之后便是根据链表将参数绑定到内存对象中。

具体的绑定方式如下

net.sourceforge.stripes.controller.DefaultActionBeanPropertyBinder#bindNonNullValue

使用 net.sourceforge.stripes.util.bean.PropertyExpressionEvaluation#setValue 进行绑定, 最后通过 net.sourceforge.stripes.util.bean.JavaBeanPropertyAccessor#setValue 落地实现参数绑定。

public void setValue(NodeEvaluation evaluation, Object bean, Object value) {
        String property = evaluation.getNode().getStringValue();
        PropertyDescriptor pd = ReflectUtil.getPropertyDescriptor(bean.getClass(), property);

        ...
            if (pd != null) {
                Method m = pd.getWriteMethod();
                ...

                m = ReflectUtil.findAccessibleMethod(m);
                m.invoke(bean, value);
            } else {
                Field field = ReflectUtil.getField(bean.getClass(), property);
                ...

                field.set(bean, value);
            }

       ...
        }
    }

如上所示,可见通过 net.sourceforge.stripes.util.ReflectUtil#getPropertyDescriptor 获取 PropertyDescriptor ,该函数的具体实现如下

public static PropertyDescriptor[] getPropertyDescriptors(Class<?> clazz) {
        if (propertyDescriptors.containsKey(clazz)) {
            Collection<PropertyDescriptor> pds = ((Map)propertyDescriptors.get(clazz)).values();
            return (PropertyDescriptor[])pds.toArray(new PropertyDescriptor[pds.size()]);
        } else {
            try {
                PropertyDescriptor[] pds = Introspector.getBeanInfo(clazz).getPropertyDescriptors();
                pds = (PropertyDescriptor[])Arrays.asList(pds).toArray(new PropertyDescriptor[pds.length]);
                ...

...

显然,在该函数中,使用了 Introspector.getBeanInfo 进行内省,同时未指定 stopclass ,出现了和springmvc相同的问题,由此可以使用 class.module.classLoader.resources.context.parent.pipeline.first 这一串参数获取到 accesslog 对象

进而设置该内存对象的属性,利用日志功能进行getshell。

StripesMVC框架是存在相同问题的,但是经过对最新版的分析,自 stripes 1.5 8 及之后,在

net.sourceforge.stripes.controller.DefaultActionBeanPropertyBinder#bind(net.sourceforge.stripes.action.ActionBean, net.sourceforge.stripes.action.ActionBeanContext, boolean)

添加了 net.sourceforge.stripes.controller.DefaultActionBeanPropertyBinder#isBindingAllowed 判断

protected boolean isBindingAllowed(PropertyExpressionEvaluation eval) {
        boolean allowed = BindingPolicyManager.getInstance(eval.getBean().getClass()).isBindingAllowed(eval);
        if (!allowed) {
            String param = eval.getExpression().getSource();
            log.warn(new Object[]{"Binding denied for parameter [", param, "]"});
        }

        return allowed;
    }

在该判断中,通过黑名单的方式过滤之前提到过的链表中的类名,黑名单如下

private static final List<Class<?>> ILLEGAL_NODE_VALUE_TYPES = Arrays.asList(ActionBeanContext.class, Class.class, ClassLoader.class, HttpSession.class, ServletRequest.class, ServletResponse.class);

如果链表中出现了

ActionBeanContext.class , Class.class , ClassLoader.class , HttpSession.class , ServletRequest.class , ServletResponse.class

这些类将直接中断解析,从而利用失败,由此关于变量自动绑定部分的分析就到此结束了。

虽然整个过程走下来最终并没有得到想要的效果,但是也希望能为其他同学在审计时提供参考。