安全中間件的設計思路和簡單實踐

最近安全中間件慢慢走熱,而技術設計類似的rasp技術火熱的同時面臨推廣落地的問題:

rasp的侵入式特性和攔截特性導致開發和運維普通不太願意配合,當生產環境出現問題時往往第一時間先把責任推給rasp,逐漸的安全部門普遍只能把rasp設置爲告警模式,而且越是大的集羣攔截開的就越少,所以字節的elkeid和某外賣大廠內部的rasp都是告警模式,沒有發揮rasp的實際作用。相反的深圳某體制內企業他們的信息系統大部分都是採購的,並採取自研的策略,但是這個企業對外每開放一個端口,都強制要求安裝網防G01進行管控。

我思考這個問題得出的答案是:
本質上rasp是安全部門推動的,對業務性來說代碼可控力度較弱,強侵入性和強攔截性導致只有話語權較強的企業才能完整落地。尤其排查問題的成本實在過高,所以導致開發、運維、安全三方技術力量在面對生產環境問題時很容易扯皮,最終rasp面臨的不是減少攔截性就是減少侵入性。

當然,後面我們再進一步解析安全中間件會發現:本質上這是一個管理問題,還真不是技術問題。

 

安全中間件的優勢是:

運維和開發由於合規因素都是相對隔離的,企業人數越多,運維和開發的隔離性就越明顯。在運維人員採購以及管控中間件的這部分工作中,安全中間件的優勢就出現了:運維部門採購安全中間件後,往往會開啓所有的安全策略,但是安全策略的關閉、調整的權限是留給開發部門的。從管理角度上運維人員已經落實了安全責任,如果開發在使用中間件時爲了業務邏輯關閉、調整中間件的安全策略,屬於是開發部門的安全問題與運維無關。

這也間接解釋了國內很多企業的安全部門尷尬的原因:安全工作要落地,但是各部門又沒有相關的能力,只能安全部門自身輸出安全能力提供安全產品。而安全預算往往又不足,只能安全部門從業務端開始從業務捋到運維,鏈路太長又氣又累,出了問題還要背鍋。但安全部門本質上是要求從業務端就開始層層履行安全責任的監管部門,而目前的現狀是運動員又是裁判員,這讓人確實很難受。

 

接下來聊聊我手工改造tomcat的一些過程,供大家欣賞:

1)準備兩套tomcat源碼,分別重命名

這樣做是因爲maven環境下的tomcat開發調試較爲方便利於長期開發,而ant是標準的編譯方案適合最終發佈

2)使用idea直接加載apache-tomcat-8.5.75-src-maven目錄

點擊modules按鈕

如下圖,選中java按鈕點擊 sources 按鈕聲明源碼目錄

將以下代碼放到pom.xml裏面然後放到源碼的根目錄中

注意:確保採用正確語言級別,tomcat8.5我採用java8的語法

<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
         xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
         xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">

  <modelVersion>4.0.0</modelVersion>
  <groupId>org.apache.tomcat</groupId>
  <artifactId>tomcat</artifactId>
  <name>tomcat</name>
  <version>8.5.75</version>
  <properties>
    <maven.compiler.target>1.8</maven.compiler.target>
    <maven.compiler.source>1.8</maven.compiler.source>
  </properties>

  <dependencies>
    <dependency>
      <groupId>junit</groupId>
      <artifactId>junit</artifactId>
      <version>4.13</version>
      <scope>test</scope>
    </dependency>
    <dependency>
      <groupId>org.apache.ant</groupId>
      <artifactId>ant</artifactId>
      <version>1.10.5</version>
    </dependency>
    <dependency>
      <groupId>wsdl4j</groupId>
      <artifactId>wsdl4j</artifactId>
      <version>1.6.3</version>
    </dependency>

    <dependency>
      <groupId>org.apache.geronimo.specs</groupId>
      <artifactId>geronimo-jaxrpc_1.1_spec</artifactId>
      <version>2.1</version>
    </dependency>

    <dependency>
      <groupId>org.eclipse.jdt</groupId>
      <artifactId>ecj</artifactId>
      <version>3.26.0</version>
    </dependency>

    <dependency>
      <groupId>org.easymock</groupId>
      <artifactId>easymock</artifactId>
      <version>4.0.2</version>
      <scope>test</scope>
    </dependency>
    <dependency>
      <groupId>biz.aQute.bnd</groupId>
      <artifactId>biz.aQute.bnd</artifactId>
      <version>5.2.0</version>
    </dependency>

  </dependencies>

  <build>
    <plugins>
      <plugin>
        <groupId>org.apache.maven.plugins</groupId>
        <artifactId>maven-compiler-plugin</artifactId>
        <configuration>
          <source>1.8</source>
          <target>1.8</target>
          <encoding>UTF-8</encoding>
        </configuration>
      </plugin>
      <plugin>
        <groupId>org.apache.maven.plugins</groupId>
        <artifactId>maven-resources-plugin</artifactId>
        <configuration>
          <encoding>UTF-8</encoding>
        </configuration>
      </plugin>
    </plugins>
  </build>
</project>

 

3)默認啓動是有問題的,我們來做如下修改

3.1)如圖刪掉examples目錄

3.2)找到 java/org/apache/catalina/startup/ContextConfig.java 的 configureStart 方法

在 webConfig(); 這句話下增加下面這句話

context.addServletContainerInitializer(new JasperInitializer(), null);

結果如下圖

3.3)修改啓動時的vm參數,防止亂碼

   

-Dfile.encoding=UTF-8
-Duser.timezone=Asia/Shanghai
-Duser.language=en

3.4)修改代碼防止亂碼

修改 java/org/apache/tomcat/util/res/StringManager.java 類中的getString函數

if (bundle != null) {
    str = bundle.getString(key);
}

改爲以下代碼,注意catch中需要增加一個異常

 try {
            // Avoid NPE if bundle is null and treat it like an MRE
            if (bundle != null) {
                //str = bundle.getString(key);
                str = new String(bundle.getString(key).getBytes("ISO-8859-1"), "UTF-8");
            }
        } catch (MissingResourceException | UnsupportedEncodingException mre) {

 

4)找到 java/org/apache/catalina/startup/Bootstrap.java 直接點擊就可以啓動了

注意:建議修改hosts文件將 123.com指向127.0.0.1方便抓包

5)接下來我們寫一個filter直接過濾Multipart和PUT動詞的上傳漏洞

在 java/org/apache/coyote/sec/SecCheckFilter.java 中寫入以下代碼,過濾邏輯請看註釋

(其實我對coyote的request和reponse都有另外的私有化修改,所以直接把sec代碼放在coyote下最爲方便)

package org.apache.coyote.sec;

import org.apache.catalina.filters.RequestFilter;
import org.apache.juli.logging.Log;
import org.apache.juli.logging.LogFactory;
import org.apache.tomcat.util.http.fileupload.FileItem;
import org.apache.tomcat.util.http.fileupload.disk.DiskFileItemFactory;
import org.apache.tomcat.util.http.fileupload.servlet.ServletFileUpload;
import org.apache.tomcat.util.http.fileupload.servlet.ServletRequestContext;

import javax.servlet.FilterChain;
import javax.servlet.ServletException;
import javax.servlet.ServletRequest;
import javax.servlet.ServletResponse;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
import java.util.List;
import java.util.Locale;

/* web.xml註解
  <filter>
    <filter-name>SecCheckFilter</filter-name>
    <filter-class>
        org.apache.coyote.sec.SecCheckFilter
    </filter-class>
    <async-supported>true</async-supported>
  </filter>
  <filter-mapping>
    <filter-name>SecCheckFilter</filter-name>
    <url-pattern>/*</url-pattern>
  </filter-mapping>
 */
public final class SecCheckFilter extends RequestFilter {

    private final Log log = LogFactory.getLog(SecCheckFilter.class);

    @Override
    public void doFilter(ServletRequest request, ServletResponse response,
                         FilterChain chain) throws IOException, ServletException {

        request.setCharacterEncoding("UTF-8");
        HttpServletRequest httpRequest = (HttpServletRequest) request;
        
        //防止getParameter與getInputStream衝突
        httpRequest.getParameterMap();
        httpRequest = new BufferedServletRequestWrapper(httpRequest);

        response.setCharacterEncoding("UTF-8");
        String httpMethod = httpRequest.getMethod().toLowerCase(Locale.ROOT);
        // 禁用 get post options 之外的其他http請求,防止 put move 等上傳攻擊
        try {
            switch (httpMethod) {
                case "get":
                case "post":
                case "options":
                    break;
                default:
                    ((HttpServletResponse) response).sendError(HttpServletResponse.SC_METHOD_NOT_ALLOWED);
                    return;
            }

            boolean isMultipart = ServletFileUpload.isMultipartContent(httpRequest);
            if (isMultipart) {
                // 校驗Multipart上傳時的文件後綴
                DiskFileItemFactory factory = new DiskFileItemFactory();
                ServletFileUpload upload = new ServletFileUpload(factory);
                List<FileItem> fileItems;

                fileItems = upload.parseRequest(new ServletRequestContext(httpRequest));
                if (fileItems != null && fileItems.size() > 0) {
                    //遍歷Multipart入參
                    for (FileItem item : fileItems) {
                        if (!item.isFormField()) {
                            String FileName = item.getName().toLowerCase(Locale.ROOT);
                            // 校驗文件名中的特殊字符
                            if (FileName.contains("/") || FileName.contains("\\") || FileName.contains(":") || FileName.contains("*")
                                || FileName.contains("?") || FileName.contains("\"") || FileName.contains("<") || FileName.contains(">")
                                || FileName.contains("|")   // windows文件名禁用 / \ : * ? " < > |
                            ) {
                                ((HttpServletResponse) response).sendError(HttpServletResponse.SC_BAD_REQUEST);
                                return;
                            }
                            // 校驗文件後綴
                            String Extension = FileName.substring(FileName.lastIndexOf(".") + 1).trim();
                            if (Extension.startsWith("js")  // jsp jspx js
                                || Extension.startsWith("asp") // asp aspx
                                || Extension.startsWith("jar") // jar
                                || Extension.startsWith("war") // war
                                || Extension.startsWith("php") // php
                                || Extension.startsWith("htm") // htm html
                                || Extension.startsWith("shtm") // shtml
                                || Extension.startsWith("exe") // exe
                                || Extension.startsWith("bat") // bat
                            ) {
                                ((HttpServletResponse) response).sendError(HttpServletResponse.SC_BAD_REQUEST);
                                return;
                            }
                        }
                    }
                }
            }
            // 全部使用wrapper進行處理
            chain.doFilter(httpRequest, response);

        } catch (Exception e) {
        }
    }

    @Override
    protected Log getLogger() {
        return log;
    }
}

這裏有幾個注意點:

5.1)文件上傳的stream讀完一次後就使用完畢,而我們需要複用request流,要做如下處理

新增 java/org/apache/coyote/sec/BufferedServletInputStream.java

package org.apache.coyote.sec;

import javax.servlet.ReadListener;
import javax.servlet.ServletInputStream;
import java.io.ByteArrayInputStream;
import java.io.IOException;

public class BufferedServletInputStream extends ServletInputStream {
    private final ByteArrayInputStream inputStream;

    public BufferedServletInputStream(byte[] buffer) {
        this.inputStream = new ByteArrayInputStream(buffer);
    }

    @Override
    public int available() throws IOException {
        return inputStream.available();
    }

    @Override
    public int read() throws IOException {
        return inputStream.read();
    }

    @Override
    public int read(byte[] b, int off, int len) throws IOException {
        return inputStream.read(b, off, len);
    }

    @Override
    public boolean isFinished() {
        return false;
    }

    @Override
    public boolean isReady() {
        return false;
    }

    @Override
    public void setReadListener(ReadListener listener) {

    }
}

新增  java/org/apache/coyote/sec/BufferedServletRequestWrapper.java

package org.apache.coyote.sec;


import javax.servlet.ServletInputStream;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletRequestWrapper;
import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.io.InputStream;

public class BufferedServletRequestWrapper extends HttpServletRequestWrapper {
    private final byte[] buffer;

    public BufferedServletRequestWrapper(HttpServletRequest request) throws IOException {
        super(request);
        InputStream is = request.getInputStream();
        ByteArrayOutputStream baos = new ByteArrayOutputStream();
        byte[] buff = new byte[1024];
        int read;
        while ((read = is.read(buff)) > 0) {
            baos.write(buff, 0, read);
        }
        this.buffer = baos.toByteArray();
    }

    @Override
    public ServletInputStream getInputStream() throws IOException {
        return new BufferedServletInputStream(this.buffer);
    }
}

同時在conf目錄下的web.xml中引入filter

  <filter>
    <filter-name>SecCheckFilter</filter-name>
    <filter-class>
        org.apache.coyote.sec.SecCheckFilter
    </filter-class>
    <async-supported>true</async-supported>
  </filter>
  <filter-mapping>
    <filter-name>SecCheckFilter</filter-name>
    <url-pattern>/*</url-pattern>
  </filter-mapping>

 

6)試驗一下,成功

 

7)編譯apache-tomcat-8.5.75-src-ant發佈

在環境變量中準備好ant

ANT_HOME=C:\Java\apache-ant-1.9.15

再將我們在maven工程中調試成功的代碼放入ant工程中的同名目錄

以及在web.xml放入filter的聲明

  <filter>
    <filter-name>SecCheckFilter</filter-name>
    <filter-class>
        org.apache.coyote.sec.SecCheckFilter
    </filter-class>
    <async-supported>true</async-supported>
  </filter>
  <filter-mapping>
    <filter-name>SecCheckFilter</filter-name>
    <url-pattern>/*</url-pattern>
  </filter-mapping>

最後在根目錄下執行ant命令

ant package-zip

我們可以獲得下面這個壓縮包

 

8)最終試驗,以圖爲證:

可以看到依然是提示400錯誤,並且tomcat給出的信息與開發模式不一樣了。

 

再接下來,我們增加一個語義waf的防護策略:

9)java的servlet在獲取參數時使用以下的標準:

9.1) 獲取請求方式
request.getMethod();    get和post都可用

9.2) 獲取請求類型
request.getContentType();   get和post都可用,示例值:application/json ,multipart/form-data, application/xml等

9.3) 獲取所有參數key
request.getParameterNames();   get和post都可用,注:不適用contentType爲multipart/form-data

9.4) 獲取參數值value
request.getParameter("test");   get和post都可用,注:不適用contentType爲multipart/form-data

9.5) 獲取取參數請求集合
request.getParameterMap();   get和post都可用,注: 不適用contentType爲multipart/form-data

總結:multipart和普通的文本是分開校驗的,同時multipart中isFormField方法判斷當前是上傳文件還是參數輸入,所以我們要增加一個對multipart中輸入參數的sql注入校驗。

9.6)我之前寫過一篇文章 https://my.oschina.net/9199771/blog/5085337

下文中我使用的文件是: https://github.com/k4n5ha0/libinjection-Java/blob/master/src/main/java/SqlParse.java

所以在sec目錄下我們將waf代碼SqlParse.java複製進去(代碼過長文章就不放進去了,大家到倉庫裏去看就行了)

最終在校驗http動詞後:對所有輸入參數和multipart的輸入參數,增加語義waf的策略

package org.apache.coyote.sec;

import org.apache.catalina.filters.RequestFilter;
import org.apache.juli.logging.Log;
import org.apache.juli.logging.LogFactory;
import org.apache.tomcat.util.http.fileupload.FileItem;
import org.apache.tomcat.util.http.fileupload.disk.DiskFileItemFactory;
import org.apache.tomcat.util.http.fileupload.servlet.ServletFileUpload;
import org.apache.tomcat.util.http.fileupload.servlet.ServletRequestContext;

import javax.servlet.FilterChain;
import javax.servlet.ServletException;
import javax.servlet.ServletRequest;
import javax.servlet.ServletResponse;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
import java.util.Enumeration;
import java.util.List;
import java.util.Locale;

/* web.xml註解
  <filter>
    <filter-name>SecCheckFilter</filter-name>
    <filter-class>
        org.apache.coyote.sec.SecCheckFilter
    </filter-class>
    <async-supported>true</async-supported>
  </filter>
  <filter-mapping>
    <filter-name>SecCheckFilter</filter-name>
    <url-pattern>/*</url-pattern>
  </filter-mapping>
 */
public final class SecCheckFilter extends RequestFilter {

    private final Log log = LogFactory.getLog(SecCheckFilter.class);

    @Override
    public void doFilter(ServletRequest request, ServletResponse response,
                         FilterChain chain) throws IOException, ServletException {

        HttpServletRequest httpRequest = (HttpServletRequest) request;
        httpRequest = new BufferedServletRequestWrapper(httpRequest);
        response.setCharacterEncoding("UTF-8");
        String httpMethod = ((HttpServletRequest) request).getMethod().toLowerCase(Locale.ROOT);
        // 禁用 get post options 之外的其他http請求,防止 put move 等上傳攻擊
        try {
            switch (httpMethod) {
                case "get":
                case "post":
                case "options":
                    break;
                default:
                    ((HttpServletResponse) response).sendError(HttpServletResponse.SC_METHOD_NOT_ALLOWED);
                    return;
            }

            // 使用語義waf策略過濾所有非Multipart提交的輸入參數
            Enumeration<String> enums = httpRequest.getParameterNames();
            while (enums.hasMoreElements()) {
                String pn = enums.nextElement();
                String[] vales = httpRequest.getParameterValues(pn);
                // i=1&i=2&i=3 這種情況下,所有的值也都必須過濾一次
                for (String vale : vales) {
                    if (vale.length() > 5 && SqlParse.isSQLi(vale)) {
                        ((HttpServletResponse) response).sendError(HttpServletResponse.SC_BAD_REQUEST);
                        return;
                    }
                }
            }

            boolean isMultipart = ServletFileUpload.isMultipartContent(httpRequest);
            if (isMultipart) {
                // 校驗Multipart上傳時的文件後綴
                DiskFileItemFactory factory = new DiskFileItemFactory();
                ServletFileUpload upload = new ServletFileUpload(factory);
                List<FileItem> fileItems;

                fileItems = upload.parseRequest(new ServletRequestContext(httpRequest));
                if (fileItems != null && fileItems.size() > 0) {
                    //遍歷Multipart入參
                    for (FileItem item : fileItems) {
                        if (!item.isFormField()) {
                            String FileName = item.getName().toLowerCase(Locale.ROOT);
                            // 校驗文件名中的特殊字符
                            if (FileName.contains("/") || FileName.contains("\\") || FileName.contains(":") || FileName.contains("*")
                                || FileName.contains("?") || FileName.contains("\"") || FileName.contains("<") || FileName.contains(">")
                                || FileName.contains("|")   // windows文件名禁用 / \ : * ? " < > |
                            ) {
                                ((HttpServletResponse) response).sendError(HttpServletResponse.SC_BAD_REQUEST);
                                return;
                            }
                            // 校驗文件後綴
                            String Extension = FileName.substring(FileName.lastIndexOf(".") + 1).trim();
                            if (Extension.startsWith("js")  // jsp jspx js
                                || Extension.startsWith("asp") // asp aspx
                                || Extension.startsWith("jar") // jar
                                || Extension.startsWith("war") // war
                                || Extension.startsWith("php") // php
                                || Extension.startsWith("htm") // htm html
                                || Extension.startsWith("shtm") // shtml
                                || Extension.startsWith("exe") // exe
                                || Extension.startsWith("bat") // bat
                            ) {
                                ((HttpServletResponse) response).sendError(HttpServletResponse.SC_BAD_REQUEST);
                                return;
                            }
                        }
                        // 校驗Multipart入參時的sql攻擊
                        else {
                            if (item.getString().length() > 5 && SqlParse.isSQLi(item.getString())) {
                                ((HttpServletResponse) response).sendError(HttpServletResponse.SC_BAD_REQUEST);
                                return;
                            }
                        }
                    }
                }
            }
            // 全部使用wrapper進行處理
            chain.doFilter(httpRequest, response);

        } catch (Exception e) {
        }
    }

    @Override
    protected Log getLogger() {
        return log;
    }
}

備註:

通過研究modsecurity的策略發現:

使用以下正則判斷單個變量存在連續3個或以上的英文特殊字符可以很好的避免sql注入:

// "mzQ1BAN<'\">cjkNLJ" 是sqlmap探測數據庫種類的payload
if (Pattern.compile("[`~!@#$%^&*()_+-=,.<>/?;:\\[\\]{}'\"]{3,}").matcher("mzQ1BAN<'\">cjkNLJ").find()) {
    System.out.println("sqli!");
    return;
}

之後參考以下waf的bypass問題:

https://mp.weixin.qq.com/s/GM1YDKB_04sDvZR3ar7d3A

可以得出sql轉換成ast語法樹的結果如果與web防火牆、db防火牆的結果如果不一致,將會導致防護失效的問題。

所以我們可以得出最合理的sql注入防護應當在數據庫底層集成:

https://mariadb.com/kb/en/maxscale-23-filters/

https://www.2ndquadrant.com/en/blog/how-to-protect-your-postgresql-databases-from-cyberattacks-with-sql-firewall/

https://dev.mysql.com/doc/refman/8.0/en/firewall-usage.html

或連接池集成sql注入防護能力,參考Druid的sql注入防護:

https://www.bookstack.cn/read/Druid/ffdd9118e6208531.md

 

10)增加反序列化黑名單

通過之前的研究成果《jdk反序列化安全防護研究:續》: https://my.oschina.net/9199771/blog/5125687

結合工作中的經驗,使用以下jvm參數可以獲得良好的體驗(G1算法在jdk11中是默認的,可以去掉)

-XX:+UseG1GC -Dfile.encoding=UTF-8 -Duser.timezone=Asia/Shanghai -Duser.language=en

10.1)windows環境下對應的setclasspath.bat中在

if ""%1"" == ""debug"" goto needJavaHome

上方填加以下代碼

set "JAVA_OPTS=%JAVA_OPTS% -XX:+UseG1GC -Dfile.encoding=UTF-8 -Duser.timezone=Asia/Shanghai -Duser.language=en"
set "JAVA_OPTS=%JAVA_OPTS% -Djdk.serialFilter=maxarray=5000;!java.util.PriorityQueue;!org.apache.commons.collections.functors.ChainedTransformer;!org.apache.commons.collections.functors.InvokerTransformer;!org.apache.commons.collections.functors.InstantiateTransformer;!org.apache.commons.collections4.functors.InvokerTransformer;!org.apache.commons.collections4.functors.InstantiateTransformer;!org.codehaus.groovy.runtime.ConvertedClosure;!org.codehaus.groovy.runtime.MethodClosure;!org.springframework.beans.factory.ObjectFactory;!com.sun.org.apache.xalan.internal.xsltc.trax.TemplatesImpl;!org.apache.xalan.xsltc.trax.TemplatesImpl;!com.mchange.v2.c3p0.impl.PoolBackedDataSourceBase;"

 10.2)linux環境下對應的setclasspath.sh中在

if [ -z "$JAVA_HOME" ] && [ -z "$JRE_HOME" ]; then

上方填加以下代碼,並且多了一個禁止root啓動的判斷

if [[ $EUID -eq 0 ]]; then
	echo "Error:tomcat can't be run as root!" 1>&2
	exit 1
fi
JAVA_OPTS="$JAVA_OPTS -XX:+UseG1GC -Dfile.encoding=UTF-8 -Duser.timezone=Asia/Shanghai -Duser.language=en"
JAVA_OPTS="$JAVA_OPTS -Djdk.serialFilter=maxarray=5000;!java.util.PriorityQueue;\
    !org.apache.commons.collections.functors.ChainedTransformer;\
    !org.apache.commons.collections.functors.InvokerTransformer;\
    !org.apache.commons.collections.functors.InstantiateTransformer;\
    !org.apache.commons.collections4.functors.InvokerTransformer;\
    !org.apache.commons.collections4.functors.InstantiateTransformer;\
    !org.codehaus.groovy.runtime.ConvertedClosure;\
    !org.codehaus.groovy.runtime.MethodClosure;\
    !org.springframework.beans.factory.ObjectFactory;\
    !com.sun.org.apache.xalan.internal.xsltc.trax.TemplatesImpl;\
    !org.apache.xalan.xsltc.trax.TemplatesImpl;\
    !com.mchange.v2.c3p0.impl.PoolBackedDataSourceBase;"

 

11)對server.xml進行修改增強安全性

11.1)unpackWARs設置爲false可以減少攻擊面,autoDeploy是否修改爲false取決於devops的流程設計

<Host name="localhost" appBase="webapps" unpackWARs="false" autoDeploy="true">

11.2)隱藏tomcat報錯信息和版本信息,順便說一句網上很多隱藏版本信息的方法不是官方標準的

<Valve className="org.apache.catalina.valves.ErrorReportValve" showReport="false" showServerInfo="false" />

修改好後的結果如下圖

 

12)接下來講一個很有趣的設計,利用filter對輸出內容進行過濾

BufferedServletResponseWrapper.java 是新增的代理類,作用類似於請求的代理類

package org.apache.coyote.sec;

import javax.servlet.ServletOutputStream;
import javax.servlet.WriteListener;
import javax.servlet.http.HttpServletResponse;
import javax.servlet.http.HttpServletResponseWrapper;
import java.io.*;

public class BufferedServletResponseWrapper extends HttpServletResponseWrapper {
    private ByteArrayOutputStream buffer;
    private ServletOutputStream out;
    private PrintWriter writer;

    public BufferedServletResponseWrapper(HttpServletResponse resp) throws IOException {
        super(resp);
        buffer = new ByteArrayOutputStream();//真正存儲數據的流
        out = new WapperedOutputStream(buffer);
        writer = new PrintWriter(new OutputStreamWriter(buffer));
    }

    // 過濾響應包的head頭
    @Override
    public void setHeader(String name, String value) {
        if ("allow".equalsIgnoreCase(name)) {
            return;
        }
        if ("server".equalsIgnoreCase(name)) {
            return;
        }
        if ("WWW-Authenticate".equalsIgnoreCase(name)) {
            return;
        }
        super.setHeader(name, value);
    }

    // 過濾響應包的head頭
    @Override
    public void addHeader(String name, String value) {
        if ("allow".equalsIgnoreCase(name)) {
            return;
        }
        if ("server".equalsIgnoreCase(name)) {
            return;
        }
        if ("WWW-Authenticate".equalsIgnoreCase(name)) {
            return;
        }
        super.setHeader(name, value);
    }

    //重載父類獲取outputstream的方法
    @Override
    public ServletOutputStream getOutputStream() throws IOException {
        return out;
    }

    //重載父類獲取writer的方法
    @Override
    public PrintWriter getWriter() throws UnsupportedEncodingException {
        return writer;
    }

    //重載父類獲取flushBuffer的方法
    @Override
    public void flushBuffer() throws IOException {
        if (out != null) {
            out.flush();
        }
        if (writer != null) {
            writer.flush();
        }
    }

    @Override
    public void reset() {
        buffer.reset();
    }

    public String getContentString() throws IOException {
        //將out和writer中的數據強制輸出到WapperedResponse的buffer裏面,否則取不到數據
        flushBuffer();
        return buffer.toString();
    }

    public byte[] getContentBytes() throws IOException {
        //將out和writer中的數據強制輸出到WapperedResponse的buffer裏面,否則取不到數據
        flushBuffer();
        return buffer.toByteArray();
    }

    //內部類,對ServletOutputStream進行包裝
    private class WapperedOutputStream extends ServletOutputStream {
        private ByteArrayOutputStream bos;

        public WapperedOutputStream(ByteArrayOutputStream stream) {
            bos = stream;
        }

        @Override
        public void write(int b) throws IOException {
            bos.write(b);
        }

        @Override
        public boolean isReady() {
            return false;
        }

        @Override
        public void setWriteListener(WriteListener listener) {

        }
    }
}

如上所示我們重寫了setheader和addheader兩個方法以防止輸出一些等保測評中導致無法通過的http響應頭

備註:也可以直接將安全邏輯寫到底層的response類中,做到全面防護

 

對應的我們的過濾器代碼也發生了變化,具體邏輯請看代碼中的註釋:

package org.apache.coyote.sec;

import org.apache.catalina.filters.RequestFilter;
import org.apache.juli.logging.Log;
import org.apache.juli.logging.LogFactory;
import org.apache.tomcat.util.http.fileupload.FileItem;
import org.apache.tomcat.util.http.fileupload.disk.DiskFileItemFactory;
import org.apache.tomcat.util.http.fileupload.servlet.ServletFileUpload;
import org.apache.tomcat.util.http.fileupload.servlet.ServletRequestContext;

import javax.servlet.*;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
import java.util.Enumeration;
import java.util.List;
import java.util.Locale;

/* web.xml註解
  <filter>
    <filter-name>SecCheckFilter</filter-name>
    <filter-class>
        org.apache.coyote.sec.SecCheckFilter
    </filter-class>
    <async-supported>true</async-supported>
  </filter>
  <filter-mapping>
    <filter-name>SecCheckFilter</filter-name>
    <url-pattern>/*</url-pattern>
  </filter-mapping>
 */
public final class SecCheckFilter extends RequestFilter {

    private final Log log = LogFactory.getLog(SecCheckFilter.class);

    @Override
    public void doFilter(ServletRequest request, ServletResponse response,
                         FilterChain chain) throws IOException, ServletException {

        HttpServletRequest httpRequest = (HttpServletRequest) request;
        httpRequest = new BufferedServletRequestWrapper(httpRequest);
        // 對響應數據使用wrapper進行代理
        BufferedServletResponseWrapper httpResponse = new BufferedServletResponseWrapper((HttpServletResponse) response);
        response.setCharacterEncoding("UTF-8");
        String httpMethod = ((HttpServletRequest) request).getMethod().toLowerCase(Locale.ROOT);
        // 禁用 get post options 之外的其他http請求,防止 put move 等上傳攻擊
        try {
            switch (httpMethod) {
                case "get":
                case "post":
                case "options":
                    break;
                default:
                    ((HttpServletResponse) response).sendError(HttpServletResponse.SC_METHOD_NOT_ALLOWED);
                    return;
            }

            // 使用語義waf策略過濾所有非Multipart提交的輸入參數
            Enumeration<String> enums = httpRequest.getParameterNames();
            while (enums.hasMoreElements()) {
                String pn = enums.nextElement();
                String[] vales = httpRequest.getParameterValues(pn);
                // i=1&i=2&i=3 這種情況下,所有的值也都必須過濾一次
                for (String vale : vales) {
                    if (vale.length() > 5 && SqlParse.isSQLi(vale)) {
                        ((HttpServletResponse) response).sendError(HttpServletResponse.SC_BAD_REQUEST);
                        return;
                    }
                }
            }

            boolean isMultipart = ServletFileUpload.isMultipartContent(httpRequest);
            if (isMultipart) {
                // 校驗Multipart上傳時的文件後綴
                DiskFileItemFactory factory = new DiskFileItemFactory();
                ServletFileUpload upload = new ServletFileUpload(factory);
                List<FileItem> fileItems;

                fileItems = upload.parseRequest(new ServletRequestContext(httpRequest));
                if (fileItems != null && fileItems.size() > 0) {
                    //遍歷Multipart入參
                    for (FileItem item : fileItems) {
                        if (!item.isFormField()) {
                            String FileName = item.getName().toLowerCase(Locale.ROOT);
                            // 校驗文件名中的特殊字符
                            if (FileName.contains("/") || FileName.contains("\\") || FileName.contains(":") || FileName.contains("*")
                                || FileName.contains("?") || FileName.contains("\"") || FileName.contains("<") || FileName.contains(">")
                                || FileName.contains("|")   // windows文件名禁用 / \ : * ? " < > |
                            ) {
                                ((HttpServletResponse) response).sendError(HttpServletResponse.SC_BAD_REQUEST);
                                return;
                            }
                            // 校驗文件後綴
                            String Extension = FileName.substring(FileName.lastIndexOf(".") + 1).trim();
                            if (Extension.startsWith("js")  // jsp jspx js
                                || Extension.startsWith("asp") // asp aspx
                                || Extension.startsWith("jar") // jar
                                || Extension.startsWith("war") // war
                                || Extension.startsWith("php") // php
                                || Extension.startsWith("htm") // htm html
                                || Extension.startsWith("shtm") // shtml
                                || Extension.startsWith("exe") // exe
                                || Extension.startsWith("bat") // bat
                            ) {
                                ((HttpServletResponse) response).sendError(HttpServletResponse.SC_BAD_REQUEST);
                                return;
                            }
                        }
                        // 校驗Multipart入參時的sql攻擊
                        else {
                            if (item.getString().length() > 5 && SqlParse.isSQLi(item.getString())) {
                                ((HttpServletResponse) response).sendError(HttpServletResponse.SC_BAD_REQUEST);
                                return;
                            }
                        }
                    }
                }
            }
            // 全部使用wrapper進行處理
            chain.doFilter(httpRequest, httpResponse);

            // 請求的filter邏輯走完,就開始處理響應流
            byte[] content = httpResponse.getContentBytes();
            if (content.length > 0) {
                // 如果存在敏感的sql關鍵詞則輸出錯誤,防止sql注入
                String str = httpResponse.getContentString();
                if (str.contains("You have an error in your SQL syntax")) {
                    ((HttpServletResponse) httpResponse).sendError(HttpServletResponse.SC_BAD_REQUEST);
                    return;
                }
                // 正常輸出響應流的內容
                ServletOutputStream out = response.getOutputStream();
                out.write(content);
                out.flush();
            }

        } catch (Exception e) {
        }
    }

    @Override
    protected Log getLogger() {
        return log;
    }
}

如下圖,如果響應中出現以下內容就說明存在sql報錯,應該終止響應,當然我的代碼非常不嚴謹主要是爲了展示思路所以精簡了邏輯,請勿擡槓。

我們加入屏蔽sql對外報錯的邏輯:

可以看出,sqlmap完全認不出來了,連數據庫類型都判斷不出來,這也說明了現在的openrasp和數據庫安全防火牆都採用的“屏蔽數據庫報錯”的設計是多麼的簡單有效,在增加開發人員重視軟件質量的同時又增加了攻擊者sql注入的攻擊成本。

modsecurity的sql報錯關鍵詞地址如下:

https://github.com/coreruleset/coreruleset/blob/v3.4/dev/rules/sql-errors.data

 

13)進一步替換原始的java類

java提供了名爲endorsed技術,可以的簡單理解爲-Djava.endorsed.dirs指定的目錄面放置的jar文件,將有覆蓋系統API的功能,可以把自己修改後的API打入到JVM指定的啓動API中,取而代之。

方法1:將包和類名和java自帶的一樣的類,打包成一個jar包,放入到-Djava.endorsed.dirs指定的目錄中

方法2:將包和類名和java自帶的一樣的類,打包成一個jar包,放入到 $JAVA_HOME/jre/lib/endorsed 目錄中

  1. 能夠覆蓋的類是有限制的,其中不包括java.lang包中的類,比如java.lang.String這種就不行
  2. endorsed目錄:.[jdk安裝目錄]./jre/lib/endorsed,不是jdk/lib/endorsed,目錄中放的是Jar包,不是.java或.class文件,哪怕只重寫了一個類也要打包成jar包
  3. 可以在dos模式查看修改後的效果(javac、java),在eclipse需要將運行選項中的JRE欄設置爲jre(若設置爲jdk將看不到效果)。
  4. 重寫的類必須滿足jdk中的規範,例如:自定義的ArrayList類也必須實現List等接口。
  5. 這個特性最高只支持到java8

比如我們要自定義一個CJException類,先如下圖,將lib目錄設置爲依賴目錄

之後我們如下圖這樣雙擊CJException類名,就可以看到源碼了

如下圖在tomcat源碼中建好同樣的CJException類

之後我們新建一個maven項目叫做jarx,新建jarx項目是爲了快速打包讓tomcat引用的,必須保持兩者中同名CJException類代碼必須一致

同樣在源碼目錄下放入CJException

 

我們將tomcat和jarx中的setVendorCode都做如下的修改

直接maven packge打出一個jar包

然後在tomcat項目中引用jarx的jar目錄

以調試模式啓動tomcat項目,如下圖所示可以成功下斷點

 

全文總結:

通過本文的研究,我們可以通過使用filter過濾器對tomcat進行安全加固,從輸入數據到輸出數據都能增加安全邏輯校驗,後期在本文基礎上開發:

1)json內容過濾能力

2)安全規則熱拔插能力

3)安全管控可視化能力

4)將進程沙箱化,防止越權讀取的能力

等等,可以將安全中間件進行產品化。

 

禁止轉載

謝謝!

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章