2019-10-15 CVE-2017-12615 Tomcat遠程代碼執(zhí)行漏洞分析與復(fù)現(xiàn)

漏洞概述

2017年9月19日,Apache Tomcat官方確認并修復(fù)了兩個高危漏洞,其中就有遠程代碼執(zhí)行漏洞(CVE-2017-12615)。當存在漏洞的Tomcat 運行在 Windows 主機上,且啟用了HTTP PUT請求方法,攻擊者將有可能可通過精心構(gòu)造的攻擊請求數(shù)據(jù)包向服務(wù)器上傳包含任意代碼的 JSP 的webshell文件,JSP文件中的惡意代碼將能被服務(wù)器執(zhí)行,導(dǎo)致服務(wù)器上的數(shù)據(jù)泄露或獲取服務(wù)器權(quán)限。

  • 影響范圍:Apache Tomcat 7.0.0 – 7.0.79

漏洞分析與復(fù)現(xiàn)

環(huán)境:

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

漏洞分析

查看conf/web.xml,可以發(fā)現(xiàn)tomcat默認readonlytrue,需要手動設(shè)置為false才可以出觸發(fā)此漏洞。

 <!--   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>

但是,即使是設(shè)置了readonlyfalsetomcat默認也不會允許用戶上傳jsp或者jspx一類的文件,而這個涉及到Servlet的一個處理邏輯問題。

可能有人沒接觸過Java Web,這里提一下Servlet和JSP的定義:

“Servlet”是“Server Applet”的縮寫,意為“小服務(wù)程序”或者“服務(wù)連接器”。

廣義上說,Servlet是Java的一個接口類;狹義上說,Servlet是指實現(xiàn)這個接口的類。

JSP(Java Server Pages,Java服務(wù)器頁面)是一種類似于PHP的東西,其本質(zhì)是Servlet(JSP在第一次訪問的時候會被翻譯成Servlet,再編譯成.class執(zhí)行)。

在默認情況下,tomcat使用org.apache.catalina.servlets.JspServlet類來處理后綴是jsp或者是jspx的請求,而PUT、DELETEHTTP操作和其他請求都是由org.apache.catalina.servlets.DefaultServlet來實現(xiàn)的。

所以,因為JspServlet類中沒有PUT上傳的邏輯,所以不能直接觸發(fā)。而這個漏洞實際上是通過構(gòu)造特殊的文件后綴名來繞過tomcat的檢測,改用DefaultServlet處理惡意的PUT請求,從而上傳jspwebshell。

下載tomcat的源碼,打開DefaultServlet類,可以看到doPut()方法。

Servlet中的doXxx()方法是重寫HttpServlet中的方法,例如doGet(),doPost()等等。只有在重寫了某個方法之后,這個Servlet才能支持對應(yīng)方式的請求,否則在請求的時候就會報405。

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

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

可以看到,要啟用這個doPut()方法,必須設(shè)置readOnlyfalse。如果是true就直接報錯了。

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

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

一番尋找后發(fā)現(xiàn)這個bind()rebind()方法都在FileDirContent.java文件中,查看bind()方法的源碼,實際上也是調(diào)用了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來寫入的。而創(chuàng)建文件當然是使用了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);

這是個抽象方法,在一個接口里。它的其中一個實現(xiàn)如下:

@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本身處理邏輯的問題,與使用的操作系統(tǒng)無關(guān)。所以,這樣構(gòu)造文件后綴名,就可以成功利用這個漏洞。

而實際上還有兩種后綴名也可以成功寫入,但僅限于操作系統(tǒng)是Windows的條件下:

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

要知道為什么這兩種文件名也可以,就得繼續(xù)跟進,查看FileOutputStream類的源碼,看看它具體是怎樣創(chuàng)建一個文件的。

這個類的其中一個構(gòu)造器如下:

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()方法又調(diào)用了open0()方法:

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

從這個native關(guān)鍵字可以看出,這個方法并不是使用Java實現(xiàn)的。實際上,使用了native關(guān)鍵字表明這是一個JNI(Java Native Interface)方法,這里調(diào)用的是JVM底層實現(xiàn)的C代碼。

然而我太菜了,并不懂JVM的底層實現(xiàn)以及怎么分析它的源碼,只好借用了網(wǎng)上的一張圖。最終創(chuàng)建文件是使用的圖上這個CreateFileW()函數(shù):

123.png

這個CreateFileW()函數(shù)實際上是Windows API的一部分,所以歸根結(jié)底還是Windows對于文件名處理的問題。

這也解釋了以下兩種后綴名最后都會被處理為.jsp

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

漏洞復(fù)現(xiàn)

readOnly手動設(shè)置為false之后,開啟tomcat,使用PUT方法上傳一個文件。

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

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

構(gòu)造請求并用BurpSuite抓包修改:

1567950905878.png

訪問webapps/ROOT目錄,發(fā)現(xiàn)shell.jsp文件。

1567950996722.png

訪問shell.jsp,可以發(fā)現(xiàn)成功訪問。

1567950933179.png

修補方案

  • 在不需要用到PUT請求時,將readonly屬性設(shè)置為true,避免文件上傳操作。
  • 升級tomcat。

參考

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

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時請結(jié)合常識與多方信息審慎甄別。
平臺聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點,簡書系信息發(fā)布平臺,僅提供信息存儲服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

友情鏈接更多精彩內(nèi)容