ZAP Script-base Authentication

최근에 ZAP의 2가지 기능에 대해 이야기를 드렸었습니다. 바로 Authentication SpideringAccess Control 테스트인데요. 이 2가지 기능의 핵심적인 부분은 ZAP에서 제공하는 Authentication과 User를 활용해서 로그인/로그아웃 플로우를 구현하는 것인데요.

오늘은 이 Authentication에서 ZAP Script를 기반으로 인증 플로우를 만들고 사용하는 방법에 대해 이야기할까 합니다.

Authentication Script란?

이전 글에서 보면 Authentication 처리에서 script 기반의 처리가 있는 것을 볼 수 있습니다. 바로 이 부분이 미리 작성한 Authentication Script를 이용해서 로그인을 처리하는 방법인데요. Community Scripts AddOn을 설치한 상태에서 Scripts 를 보면 아래와 같이 Authentication에 미리 정의된 스크립트가 여러개 있는 것을 확인할 수 있습니다.

이를 이용하여 미리 정의된 서비스를 쉽게 로그인할 수 있도록 처리가 가능하죠 😎

Script의 구조

스크립트를 만들기 전 꼭 알고 가야하는 함수들이 있습니다.

Function Description
getRequiredParamsNames() Authentication에서 사용할 필수 파라미터
getOptionalParamsNames() Authentication에서 사용할 옵션 파라미터
getCredentialsParamsNames() Users에서 받을 파라미터 (보통 username / password)
authenticate(helper, paramsValues, credentials) 실제 Authentication 처리 로직이 담길 함수

그러고 Authentication Scripts 중 짧은 것을 하나 봐 봅시다. (코드가 길어져서 중요하지 않은 부분은 생략했어요)

...생략...

function getRequiredParamsNames(){
    // 필수 파라미터
    return ["loginUrl"];
}

function getOptionalParamsNames(){
    // 옵션 파라미터
    return ["extraPostData"];
}

function getCredentialsParamsNames(){
    // Users 정보
    return ["username", "password"];
}

function authenticate(helper, paramsValues, credentials) {
    debugMode && print("---- Magento authentication script has started ----");

    var loginUri = new URI(paramsValues.get("loginUrl"), false);

    // Perform a GET request to the login page to get the form_key
    var get = helper.prepareMessage();
    get.setRequestHeader(new HttpRequestHeader(HttpRequestHeader.GET, loginUri, HttpHeader.HTTP10));
    helper.sendAndReceive(get);

    // Build the request body using the credentials values and the form_key
    var requestBody = "login[username]=" + encodeURIComponent(credentials.getParam("username"));
    requestBody += "&login[password]=" + encodeURIComponent(credentials.getParam("password"));
    requestBody += "&form_key=" + encodeURIComponent(formParameters["form_key"]);

    // Add any extra post data provided
    var extraPostData = paramsValues.get("extraPostData");
    if (extraPostData !== null && !extraPostData.trim().isEmpty()) {
        requestBody += "&" + extraPostData.trim();
    }

    // Perform a POST request to authenticate
    debugMode && print("POST request body built for the authentication:\n  " + requestBody.replaceAll("&", "\n  "));
    var post = helper.prepareMessage();
    post.setRequestHeader(new HttpRequestHeader(HttpRequestHeader.POST, loginUri, HttpHeader.HTTP10));
    post.setRequestBody(requestBody);
    post.getRequestHeader().setContentLength(post.getRequestBody().length());
    helper.sendAndReceive(post);

    debugMode && print("---- Magento authentication script has finished ----\n");
    return post;
}

결국 Authentication 설정과 Users에서 받은 credentials 정보를 기반으로 authenticate() 함수를 통해 로그인 과정을 실행하고 처리에 사용한 post object를 반환 하면서 인증 과정을 처리하게 됩니다. 보통 일반 스크립트나 코드로 구현할 땐 Cookie 처리 등을 해줘야하지만, ZAP Script에선 요청만 전달해주면 ZAP이 알아서 Set-Cookie 등을 처리하여 인증 상태로 동작을 수행할 수 있도록 지원해 줍니다.

Login script 만들기 (Starbucks)

그러면 간한하게 하나 만들어봅시다. 지난번에 Authentication Spidering 글을 썼을 때도 스타벅스의 로그인 페이지를 사용했으니, 이번에도 스타벅스의 로그인 페이지로 예시를 만들어봅시다. Starbucks의 로그인 요청은 아래와 같습니다.

Login form

  • https://www.starbucks.co.kr/login/login.do

Login request

POST https://www.starbucks.co.kr/interface/loginMember.do HTTP/1.1

user_id=test&user_pwd=test&captcha=

/login/login.do 페이지에 접근해서 실제 로그인 요청을 날려보면 위와 같이 username은 user_id, password는 user_pwd 으로 form 전송을 시도하게 됩니다. 성공/실패에 따라서 Response 내 SUCCESSFAIL로 구별됩니다.

그러면 아까 예시를 활용해서 코드를 작성해봤습니다. 하나하나 쓰는 것 보단 전체 코드에 주석을 다는게 좋을 것 같아서 한번에 코드로 올립니다.

/*
 * @author HAHWUL (@hahwul)
*/
// 로그인 처리에 사용할 객체/유틸 로드
var HttpRequestHeader = Java.type("org.parosproxy.paros.network.HttpRequestHeader");
var HttpHeader = Java.type("org.parosproxy.paros.network.HttpHeader");
var URI = Java.type("org.apache.commons.httpclient.URI");
var Pattern = Java.type("java.util.regex.Pattern");

var debugMode = false;

// 필수 파라미터 처리
function getRequiredParamsNames(){
    return ["loginUrl"];
}

// 옵션 파라미터 처리
function getOptionalParamsNames(){
    return ["extraPostData"];
}

// Users에서 받을 계정/패스워드 정보 매핑을 위한 처리
function getCredentialsParamsNames(){
    return ["username", "password"];
}

// 실제 인증 함수
function authenticate(helper, paramsValues, credentials) {
    debugMode && print("---- Starbucks authentication script has started ----");

		// 사용자로 부터 받은 Uri를 사용해도 되지만 고정 값이라면 그냥 전달하는 것도 방법 중 하나
    // 깔끔한 Referer 처리를 위해 로그인 페이지에 한번 붙어줍니다.
    var loginUri = new URI("https://www.starbucks.co.kr/login/login.do", false);
    var get = helper.prepareMessage();
    get.setRequestHeader(new HttpRequestHeader(HttpRequestHeader.GET, loginUri, HttpHeader.HTTP10));
    helper.sendAndReceive(get);

    // 이후 실제 로그인 요청을 위한 form 을 구성해줍니다.
    // credentials에서 Users에서 받은 파라미터 값을 얻어올 수 있어요.
    var requestBody = "user_id=" + encodeURIComponent(credentials.getParam("username"));
    requestBody += "&user_pwd=" + encodeURIComponent(credentials.getParam("password"));
    requestBody += "&captcha=";

    debugMode && print("POST request body built for the authentication:\n  " + requestBody.replaceAll("&", "\n  "));

    // POST Method로 로그인 요청을 전송합니다.
    var post = helper.prepareMessage();
    post.setRequestHeader(new HttpRequestHeader(HttpRequestHeader.POST, loginUri, HttpHeader.HTTP10));
    post.setRequestBody(requestBody);
    post.getRequestHeader().setContentLength(post.getRequestBody().length());
    helper.sendAndReceive(post);

    debugMode && print("---- Starbucks authentication script has finished ----\n");

    // Request 전송에 사용한 객체를 반환합니다. (이는 정상 로그인 식별 여부를 Authentication에서 체크하기 위해)
    return post;
}

Context 설정에서 Authentication을 Script-base로 바뀌주고 Starbucks 스크립트를 설정해줍니다.

아래 Poll the Specified URL로 로그인 성공 여부를 체크했습니다.

다 추가하면 Users에 사용자 계정을 입력해줍시다.

만약 여러 계정을 테스트한다면 Users에 여러개를 등록해서 처리할 수 있어요.

이제 User를 포함해서 Spidering 해보면, Script-base Authentication이 적용되는 것을 볼 수 있습니다.

History에서도 로그인 시도하는게 정확하게 보입니다.

다른 인증 메소드와의 차이점

Script-base authentication의 가장 좋은 장점은 한번 만들어둔 스크립트를 재 사용하고, 공유할 수 있다는 점인 것 같습니다. 일반적으로 도구에 인증을 추가하는 방법은 좀 귀찮은 감이 있습니다. 이 때 스크립트 기반의 인증을 사용하면 여러 사용자가 쉽게 인증 처리를 공유하여, 계정 기반으로 인증을 수행할 수 있어서 협업적인 부분에서 이점이 큽니다.

또한 Javascript 기반이기 때문에 아래와 같은 함수를 하나 만들어서 Response 내 동적인 부분들에 대해서도 처리가 가능합니다. (대표적으로 CSRF Token)

function parseFormParameters(response){
    var result = {};

    var regex = "<input.*name=\"(form_key)\".*value=\"([^\"]*)\"";
    var matcher = Pattern.compile(regex).matcher(response);
    while (matcher.find()) {
        result[matcher.group(1)] = matcher.group(2);
    }
    return result;
}