Reflected XSS를 쉽게 찾자 - Reflector Burp Suite Extension

오늘은 Reflected XSS를 찾는데 있어 탁월한 성능을 보여주는 Reflector에 대한 이야기를 하려고 합니다.

Reflector?

공식 Git에는 아래와 같이 소개되어 있습니다.

“Burp Suite extension is able to find reflected XSS on page in real-time while browsing on web-site and include some features as:”

딱 Reflected XSS를 찾기 위한 Extension입니다. 대충 보면 Sentinel과 비슷할 것 같지만, 이 친구는 Burp pro 버전의 Passive scan과 유사한 기능을 가집니다. Proxy를 통해 수집되는 정보에 대해 자동으로 체크해보고 결과를 분석가에게 통보해줍니다.

모든 URL에 대해 전수 조사하기 떄문에 놓칠 수 있는 부분을 잡아주지만 그만큼 다수 페이지에 의도하지 않은 요청도 발생할 수도 있습니다. 그래도 실제 다수의 페이로드를 전송하는 것이 아니기 때문에 개인적으로 아주 좋은 Extension으로 생각됩니다.

Install & Setting

With BApp Store

BApp Store에 존재합니다. Burpsuite의 BApp Store에서 Reflector로 검색하여 설치해줍시다.

With Build

Burp suite의 Extension은 자바로 만듭니다. Burp 자체가 자바 기반인 것도 한몫 하겠네요. 먼저 공식 git에서 소스코드를 받아줍니다.

git clone https://github.com/elkokc/reflector.git
cd reflector

컴파일 후 jar로 압축하여 burp에서 로드하면됩니다.

mkdir reles
javac -d reles ./src/*.java

class 파일이 생성되었으니 jar 묶어줍시다.

jar cvf reflector.jar reles

만들어진 reflector.jar 파일을 BApp Store에서 Manual Install을 통해 설치해줍니다.

Code Analysis

Extension 코드를 보면 CheckReflection.java 코드쪽에 검증 하는 로직이 있습니다. CheckReflection 이란 함수에서 파라미터에 값을 뽑아내고 이를 가지고 요청을 전송해서 문제가 있는지 체크합니다.

    public CheckReflection(Settings settings, IExtensionHelpers helpers, IHttpRequestResponse iHttpRequestResponse, IBurpExtenderCallbacks callbacks) {
                    this.settings = settings;
                    this.helpers = helpers;
                    this.callbacks = callbacks;
                    this.iHttpRequestResponse = iHttpRequestResponse;
                    this.bodyOffset = helpers.analyzeResponse(iHttpRequestResponse.getResponse()).getBodyOffset();
                }

                    public List<Map> checkResponse() {
                    List<Map> reflectedParameters = new ArrayList<>();
                    List<IParameter> parameters = helpers.analyzeRequest(iHttpRequestResponse).getParameters(); // 파라미터 가져옴
                    byte[] request = iHttpRequestResponse.getRequest();
                    for (IParameter parameter : parameters){ // 파라미터 만큼 전송
                        byte[] bytesOfParamValue = helpers.urlDecode(parameter.getValue().getBytes());
                        if (bytesOfParamValue.length > 2)
                        {
                            byte b = request[parameter.getValueStart() - 1];
                            if(parameter.getType() == IParameter.PARAM_JSON && b != QUOTE_BYTE){ // QUOTE_BYTE 여부 확인
                                continue;
                            }
                            List<int[]> listOfMatches = getMatches(iHttpRequestResponse.getResponse(), bytesOfParamValue);
                            if (!listOfMatches.isEmpty())
                            {
                    Map parameterDescription = new HashMap();
                    parameterDescription.put(NAME, parameter.getName());
                    parameterDescription.put(VALUE, parameter.getValue());
                    parameterDescription.put(TYPE, parameter.getType());
                    parameterDescription.put(VALUE_START, parameter.getValueStart());
                    parameterDescription.put(VALUE_END, parameter.getValueEnd());
                    parameterDescription.put(MATCHES, listOfMatches);
                    parameterDescription.put(REFLECTED_IN, checkWhereReflectionPlaced(listOfMatches));
                    reflectedParameters.add(parameterDescription);
                }
            }
        }
        if ( settings.getAggressiveMode() && !reflectedParameters.isEmpty() )
        {
            Aggressive scan = new Aggressive(settings, helpers, iHttpRequestResponse, callbacks, reflectedParameters);
            scan.scanReflectedParameters();
        } else if ( settings.getCheckContext() && !reflectedParameters.isEmpty() ) {
            String symbols = "",
                    body = new String(iHttpRequestResponse.getResponse()).substring(this.bodyOffset);
            ArrayList<int[]> payloadIndexes = null;
            //cycle by parameters
            for (Map parameter : reflectedParameters) {
                payloadIndexes = new ArrayList<>();

                for (int[] indexPair: (ArrayList<int[]>) parameter.get(MATCHES)) {
                    int[] tmpIndexes = new int[] { indexPair[0] - this.bodyOffset, indexPair[1] - this.bodyOffset };
                    payloadIndexes.add( tmpIndexes );
                }

                ContextAnalyzer contextAnalyzer = new ContextAnalyzer( body.toLowerCase(), payloadIndexes );
                symbols = contextAnalyzer.getIssuesForAllParameters();
                if ( symbols.length() > 0 ) {
                    parameter.put(VULNERABLE, symbols);
                }
            }
        }
        return reflectedParameters;
    }

체크하는 규칙은 ContextAnalyzer.java 에 정의되어 있습니다.

private String checksContextSecurity(String reflectedPayload, String context){
        String contextChars = null;
        switch (context) {
            case CONTEXT_OUT_OF_TAG: {
                if (reflectedPayload.contains("<")) {
                    contextChars = "<";
                }
            }
            break;
            case CONTEXT_IN_ATTRIBUTE_Q: {
                if (reflectedPayload.contains("'")) {
                    contextChars = "'";
                }
            }
            break;
            case CONTEXT_IN_ATTRIBUTE_DQ: {
                if (reflectedPayload.contains("\"")) {
                    contextChars = "\"";
                }
            }
            break;
            case CONTEXT_IN_TAG: {
                if (reflectedPayload.length() > 0)
                    contextChars = reflectedPayload;
                else
                    contextChars = "ALL";
            }
            break;
            case CONTEXT_IN_SCRIPT_TAG_STRING_Q: {
                if (reflectedPayload.contains("'")) {
                    contextChars = "'";
                }
            }
            break;
            case CONTEXT_IN_SCRIPT_TAG_STRING_DQ: {
                if (reflectedPayload.contains("\"")) {
                    contextChars = "\"";
                }
            }
            break;
            case CONTEXT_IN_SCRIPT_TAG: {
                if (reflectedPayload.length() > 0)
                    contextChars = reflectedPayload;
                else
                    contextChars = "ALL";
            }
            break;
        }
        return contextChars;
    }

체크하는 컨텍스트가 여러개 있습니다. 그래도 일반적인 XSS 스캐너보단 훨씬 적은 양의 데이터로만 체크하네요.

Conclusion

한 4년전 쯤에 Reflected XSS 테스트 쉽게하려고 툴 만들어서 돌리던 기억이 납니다. 곰곰히 생각해보면 Java로 짜서 Burp에 올렸더라면 조금 더 효과적이였을 것 같단 생각이 드네요. 편리한 툴이니 잘 활용하시되 의존하진 맙시다 :)

Reference

  • https://github.com/elkokc/reflector