2019-10-15 CVE-2017-12615 Tomcat远程代码执行漏洞分析与复现

漏洞概述

2017年9月19日,Apache Tomcat官方确认并修复了两个高危漏洞,其中就有远程代码执行漏洞(CVE-2017-12615)。当存在漏洞的Tomcat 运行在 Windows 主机上,且启用了HTTP PUT请求方法,攻击者将有可能可通过精心构造的攻击请求数据包向服务器上传包含任意代码的 JSP 的webshell文件,JSP文件中的恶意代码将能被服务器执行,导致服务器上的数据泄露或获取服务器权限。

  • 影响范围:Apache Tomcat 7.0.0 – 7.0.79

漏洞分析与复现

环境:

  • Windows8.1
  • Tomcat 7.0.56
  • JDK 1.8.0_221
  • IntelliJ IDEA 2019.1.3
  • BurpSuite 2.0.11

漏洞分析

查看conf/web.xml,可以发现tomcat默认readonlytrue,需要手动设置为false才可以出触发此漏洞。

 <!--   readonly            Is this context "read only", so HTTP           -->
  <!--                       commands like PUT and DELETE are               -->
  <!--                       rejected?  [true]                              -->

手动添加:

<init-param>
    <param-name>readonly</param-name>
    <param-name>false</param-name>
</init-param>

但是,即使是设置了readonlyfalsetomcat默认也不会允许用户上传jsp或者jspx一类的文件,而这个涉及到Servlet的一个处理逻辑问题。

可能有人没接触过Java Web,这里提一下Servlet和JSP的定义:

“Servlet”是“Server Applet”的缩写,意为“小服务程序”或者“服务连接器”。

广义上说,Servlet是Java的一个接口类;狭义上说,Servlet是指实现这个接口的类。

JSP(Java Server Pages,Java服务器页面)是一种类似于PHP的东西,其本质是Servlet(JSP在第一次访问的时候会被翻译成Servlet,再编译成.class执行)。

在默认情况下,tomcat使用org.apache.catalina.servlets.JspServlet类来处理后缀是jsp或者是jspx的请求,而PUTDELETEHTTP操作和其他请求都是由org.apache.catalina.servlets.DefaultServlet来实现的。

所以,因为JspServlet类中没有PUT上传的逻辑,所以不能直接触发。而这个漏洞实际上是通过构造特殊的文件后缀名来绕过tomcat的检测,改用DefaultServlet处理恶意的PUT请求,从而上传jspwebshell

下载tomcat的源码,打开DefaultServlet类,可以看到doPut()方法。

Servlet中的doXxx()方法是重写HttpServlet中的方法,例如doGet(),doPost()等等。只有在重写了某个方法之后,这个Servlet才能支持对应方式的请求,否则在请求的时候就会报405。

protected void doPut(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {

    if (readOnly) {
        resp.sendError(HttpServletResponse.SC_FORBIDDEN);
        return;
    }

可以看到,要启用这个doPut()方法,必须设置readOnlyfalse。如果是true就直接报错了。

第578~582行是写入文件的部分:

if (exists) {
        resources.rebind(path, newResource);
    } else {
        resources.bind(path, newResource);
    }

一番寻找后发现这个bind()rebind()方法都在FileDirContent.java文件中,查看bind()方法的源码,实际上也是调用了rebind()方法:

@Override
public void bind(String name, Object obj, Attributes attrs)
    throws NamingException {

    // Note: No custom attributes allowed

    File file = new File(base, name);
    if (file.exists())
        throw new NameAlreadyBoundException
            (sm.getString("resources.alreadyBound", name));

    rebind(name, obj, attrs);

}

查看rebind()方法的源码,其写入文件的核心部分如下:

File file = new File(base, name);

        InputStream is = null;
        if (obj instanceof Resource) {
            try {
                is = ((Resource) obj).streamContent();
            } catch (IOException e) {
                // Ignore
            }
        } else if (obj instanceof InputStream) {
            is = (InputStream) obj;
        } else if (obj instanceof DirContext) {
            if (file.exists()) {
                if (!file.delete())
                    throw new NamingException
                        (sm.getString("resources.bindFailed", name));
            }
            if (!file.mkdir())
                throw new NamingException
                    (sm.getString("resources.bindFailed", name));
        }
        if (is == null)
            throw new NamingException
                (sm.getString("resources.bindFailed", name));

        // Open os

        try {
            FileOutputStream os = null;
            byte buffer[] = new byte[BUFFER_SIZE];
            int len = -1;
            try {
                os = new FileOutputStream(file);
                while (true) {
                    len = is.read(buffer);
                    if (len == -1)
                        break;
                    os.write(buffer, 0, len);
                }
            } finally {
                if (os != null)
                    os.close();
                is.close();
            }

真正写入文件是使用FileOutputStream来写入的。而创建文件当然是使用了JavaFile类。

查看其源码:

public File(String pathname) {
        if (pathname == null) {
            throw new NullPointerException();
        }
        this.path = fs.normalize(pathname);
        this.prefixLength = fs.prefixLength(this.path);
    }

这里有一个normalize()方法比较可疑,进去看看:

public abstract String normalize(String path);

这是个抽象方法,在一个接口里。它的其中一个实现如下:

@Override
    public String normalize(String path) {
        int n = path.length();
        char slash = this.slash;
        char altSlash = this.altSlash;
        char prev = 0;
        for (int i = 0; i < n; i++) {
            char c = path.charAt(i);
            if (c == altSlash)
                return normalize(path, n, (prev == slash) ? i - 1 : i);
            if ((c == slash) && (prev == slash) && (i > 1))
                return normalize(path, n, i - 1);
            if ((c == ':') && (i > 1))
                return normalize(path, n, 0);
            prev = c;
        }
        if (prev == slash) return normalize(path, n, n - 1);
        return path;
    }

在第10行可以看到,如果文件名后面有“/”,会将其去掉。所以,使用以下文件后缀名可以成功写入.jsp文件:

  • .jsp/

这是Java本身处理逻辑的问题,与使用的操作系统无关。所以,这样构造文件后缀名,就可以成功利用这个漏洞。

而实际上还有两种后缀名也可以成功写入,但仅限于操作系统是Windows的条件下:

  • evil.jsp%20
  • evil.jsp::$DATA

要知道为什么这两种文件名也可以,就得继续跟进,查看FileOutputStream类的源码,看看它具体是怎样创建一个文件的。

这个类的其中一个构造器如下:

public FileOutputStream(File file, boolean append)
        throws FileNotFoundException
    {
        String name = (file != null ? file.getPath() : null);
        SecurityManager security = System.getSecurityManager();
        if (security != null) {
            security.checkWrite(name);
        }
        if (name == null) {
            throw new NullPointerException();
        }
        if (file.isInvalid()) {
            throw new FileNotFoundException("Invalid file path");
        }
        this.fd = new FileDescriptor();
        fd.attach(this);
        this.append = append;
        this.path = name;

        open(name, append);
    }

写入文件是使用的这个open()方法:

private void open(String name, boolean append)
        throws FileNotFoundException {
        open0(name, append);
    }

这个open()方法又调用了open0()方法:

private native void open0(String name, boolean append) throws FileNotFoundException;

从这个native关键字可以看出,这个方法并不是使用Java实现的。实际上,使用了native关键字表明这是一个JNI(Java Native Interface)方法,这里调用的是JVM底层实现的C代码。

然而我太菜了,并不懂JVM的底层实现以及怎么分析它的源码,只好借用了网上的一张图。最终创建文件是使用的图上这个CreateFileW()函数:

123.png

这个CreateFileW()函数实际上是Windows API的一部分,所以归根结底还是Windows对于文件名处理的问题。

这也解释了以下两种后缀名最后都会被处理为.jsp

  • .jsp%20
  • .jsp::$DATA

漏洞复现

readOnly手动设置为false之后,开启tomcat,使用PUT方法上传一个文件。

先改一下tomcat的端口以免和BurpSuite冲突:

<Connector port="9090" protocol="HTTP/1.1"
               connectionTimeout="20000"
               redirectPort="8443" />

构造请求并用BurpSuite抓包修改:

1567950905878.png

访问webapps/ROOT目录,发现shell.jsp文件。

1567950996722.png

访问shell.jsp,可以发现成功访问。

1567950933179.png

修补方案

  • 在不需要用到PUT请求时,将readonly属性设置为true,避免文件上传操作。
  • 升级tomcat

参考

https://paper.seebug.org/399/

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

推荐阅读更多精彩内容