動態

詳情 返回 返回

XSS檢測繞過(UTF-7編碼繞過) - 動態 詳情

📢 叮咚,現場運維來消息了,説項目被檢測到有高危漏洞,要求修復,以為就是jar安全漏洞,升級就完事了,就讓發過來看看👀,亞麻袋住了,“XSS檢測繞過(UTF-7編碼繞過)”,從沒見過啊,還是UTF-7。

image.png

image.png

怎麼搞?我電腦上的編輯器都沒找到有支持UTF-7編碼的,首先想到的,把這些信息丟給DeepSeek幫我分析看看,問Ai怎麼防禦?結果沒有我想要的方案。

然後去網絡搜索下吧,看看大家前輩們有沒解決過,果然有相關文件,但是都沒給出具體解決方案,不過也有所收穫,得到了一段UTF-7編碼的XSS注入參數(如果Get參數請求,記得對參數URL編碼)

+ADw-script+AD4-alert('UTF-7 XSS')+ADw-/script+AD4-

進入正題,結合項目代碼,想到可以用Filter過濾器對參數攔截,那就動手來吧,以項目SpringCloud Zuul為例

# xss regex
xss:
  enable: true
  regexes:
    # UTF-7編碼繞過
    - "(?i)(\\+[A-Za-z0-9+/=,]+\\-|\\+(?:ADw|AD4|AC8|ACI|AHs|AH0)-)"
import cn.hutool.core.util.BooleanUtil;
import cn.hutool.core.util.StrUtil;
import cn.hutool.json.JSON;
import cn.hutool.json.JSONUtil;
import com.framework.util.RegexUtil;
import com.netflix.zuul.ZuulFilter;
import com.netflix.zuul.context.RequestContext;
import lombok.Data;
import lombok.EqualsAndHashCode;
import lombok.extern.slf4j.Slf4j;
import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.context.annotation.Configuration;
import org.springframework.http.HttpStatus;
import org.springframework.stereotype.Component;
import org.springframework.util.StreamUtils;

import javax.annotation.PostConstruct;
import javax.servlet.ReadListener;
import javax.servlet.ServletInputStream;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletRequestWrapper;
import java.io.*;
import java.nio.charset.StandardCharsets;
import java.util.ArrayList;
import java.util.List;
import java.util.Map;
import java.util.regex.Matcher;
import java.util.regex.Pattern;

@Data
@Slf4j
@Component
@Configuration
@EqualsAndHashCode(callSuper = true)
@ConfigurationProperties(prefix = "xss")
@ConditionalOnProperty(prefix = "xss", name = "enable", havingValue = "true")
public class XssZuulFilter extends ZuulFilter {

    /**
     * 正則
     */
    private List<String> regexes = new ArrayList<>();
    private static List<Pattern> injectionPatterns = new ArrayList<>();

    @PostConstruct
    public void init() {
        log.debug("XssZuulFilter#init-regexes: {}", this.regexes);
        this.regexes.forEach(regex -> {
            injectionPatterns.add(
                    Pattern.compile(regex, Pattern.CASE_INSENSITIVE | Pattern.MULTILINE | Pattern.DOTALL)
            );
        });
    }

    @Override
    public String filterType() {
        return "pre"; // 在路由之前執行
    }

    @Override
    public int filterOrder() {
        return 0; // 高優先級執行
    }

    @Override
    public boolean shouldFilter() {
        return true; // 對所有請求進行過濾
    }

    @Override
    public Object run() {
        RequestContext ctx = RequestContext.getCurrentContext();
        HttpServletRequest request = ctx.getRequest();

        String requestURI = request.getRequestURI();
        // 排除ureport預覽時“過期”報錯
        if (!RegexUtil.pathAnyMatch(requestURI, "/api-config/ureport/designer/savePreviewData")) {
            // 1. 檢查URL參數
            checkUrlParameters(request, ctx);

            // 2. 檢查請求體
            if (isJsonRequest(request)) {
                checkJsonRequestBody(request, ctx);
            }
        }

        return null;
    }

    /**
     * 檢查請求參數
     *
     * @param request HttpServletRequest
     * @param ctx     RequestContext
     */
    private void checkUrlParameters(HttpServletRequest request, RequestContext ctx) {
        Map<String, String[]> params = request.getParameterMap();
        for (Map.Entry<String, String[]> entry : params.entrySet()) {
            for (String value : entry.getValue()) {
                String result = containsInjection(value);
                if (result != null) {
                    log.debug("XssZuulFilter#checkUrlParameters-result: {}", result);
                    blockRequest(ctx, String.format("請求參數包含敏感內容: 『%s』,請修正後再次請求。", result));
                    return;
                }
            }
        }
    }

    /**
     * 檢查請求JSON體
     *
     * @param request HttpServletRequest
     * @param ctx     RequestContext
     */
    private void checkJsonRequestBody(HttpServletRequest request, RequestContext ctx) {
        try (InputStream in = request.getInputStream()) {
            if (in != null) {
                String result = null;
                String body = StreamUtils.copyToString(in, StandardCharsets.UTF_8);
                log.debug("XssZuulFilter#checkJsonRequestBody-body: {}", body);

                // 先解析JSON,然後檢查每個值
                if (StrUtil.isNotEmpty(body)) {
                    if (JSONUtil.isJson(body)) {
                        JSON json = JSONUtil.parse(body);
                        log.debug("XssZuulFilter#checkJsonRequestBody-json: {}", json);
                        result = checkJsonObject(json);
                    } else { // 不是有效JSON,直接檢查原始內容
                        result = containsInjection(body);
                    }
                }

                if (result != null) {
                    log.debug("XssZuulFilter#checkJsonRequestBody-result: {}", result);
                    blockRequest(ctx, String.format("請求數據包含敏感內容: 『%s』,請修正後再次提交。", result));
                    return;
                }

                // 重新寫入請求體
                ctx.setRequest(new CustomHttpServletRequestWrapper(request, body));
            }
        } catch (IOException e) {
            e.printStackTrace();
            blockRequest(ctx, "敏感內容檢查失敗: " + e.getMessage());
        }
    }

    /**
     * 檢查json體
     *
     * @param json
     * @return
     */
    private String checkJsonObject(Object json) {
        String result = null;
        // 處理JSON對象
        if (json instanceof Map) {
            Map<?, ?> map = (Map<?, ?>) json;
            log.debug("XssZuulFilter#checkJsonObject-map: {}", map);
            for (Map.Entry<?, ?> entry : map.entrySet()) {
                if (entry.getValue() instanceof String) {
                    result = containsInjection((String) entry.getValue());
                } else if (entry.getValue() != null) {
                    result = checkJsonObject(entry.getValue());
                }
                if (result != null) {
                    return result;
                }
            }
        } else if (json instanceof List) { // 處理JSON數組
            List<?> list = (List<?>) json;
            log.debug("XssZuulFilter#checkJsonObject-list: {}", list);
            for (Object item : list) {
                if (item instanceof String) {
                    result = containsInjection((String) item);
                } else if (item != null) {
                    result = checkJsonObject(item);
                }
                if (result != null) {
                    return result;
                }
            }
        }

        return result;
    }

    /**
     * 是否JSON請求
     *
     * @param request HttpServletRequest
     * @return true/false
     */
    private boolean isJsonRequest(HttpServletRequest request) {
        String contentType = request.getContentType();
        return contentType != null && contentType.contains("application/json");
    }

    /**
     * 匹配正則
     *
     * @param input 輸入內容
     * @return null / 匹配的group(0)
     */
    private String containsInjection(String input) {
        if (StrUtil.isEmpty(input)) {
            return null;
        }

        // 跳過純數字和布爾值
        if (
                BooleanUtil.or(
                        input.matches("^\\d+\\.?\\d*$"),
                        input.equalsIgnoreCase("true"),
                        input.equalsIgnoreCase("false")
                )
        ) {
            return null;
        }

        for (Pattern pattern : injectionPatterns) {
            Matcher matcher = pattern.matcher(input);
            if (matcher.find()) {
                log.debug("XssZuulFilter#containsInjection-input: {}", input);
                return matcher.group(0);
            }
        }

        return null;
    }

    /**
     * 輸入消息提示
     *
     * @param ctx     RequestContext
     * @param message 消息內容
     */
    private void blockRequest(RequestContext ctx, String message) {
        ResponseWrapper<Boolean, ?> responseWrapper = new ResponseWrapper<>();
        responseWrapper.setStatus(false)
                .setMessage(message);
        ctx.setSendZuulResponse(false); // 不進行路由
        ctx.setResponseStatusCode(HttpStatus.OK.value()); // Bad Request
        ctx.setResponseBody(JSONUtil.toJsonStr(responseWrapper));
        ctx.getResponse().setContentType("application/json;charset=UTF-8");
    }

    /**
     * 重新寫入請求體 對於請求流
     */
    private static class CustomHttpServletRequestWrapper extends HttpServletRequestWrapper {

        private final String body;

        CustomHttpServletRequestWrapper(HttpServletRequest request, String body) {
            super(request);
            this.body = body;
        }

        @Override
        public ServletInputStream getInputStream() throws IOException {
            final ByteArrayInputStream byteArrayInputStream = new ByteArrayInputStream(body.getBytes());
            return new ServletInputStream() {
                @Override
                public boolean isFinished() {
                    return byteArrayInputStream.available() == 0;
                }

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

                @Override
                public void setReadListener(ReadListener readListener) {
                    throw new UnsupportedOperationException();
                }

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

        @Override
        public BufferedReader getReader() throws IOException {
            return new BufferedReader(new InputStreamReader(this.getInputStream()));
        }
    }
}

寫完收工,可以愉快地摸魚了🎉!

user avatar mannayang 頭像 king_wenzhinan 頭像 xiaoniuhululu 頭像 journey_64224c9377fd5 頭像 u_16502039 頭像 debuginn 頭像 seazhan 頭像 xuxueli 頭像 AmbitionGarden 頭像 lenglingx 頭像 u_15702012 頭像 ahahan 頭像
點贊 45 用戶, 點贊了這篇動態!
點贊

Add a new 評論

Some HTML is okay.