Zest와 ZAP! 강력한 보안 테스트 루틴을 만들어봐요 ⚡️

What is Zest

Zest는 Mozilla 보안팀에서 만든 JSON 기반의 스크립팅 언어입니다. 보다 쉬운 웹 테스팅을 위해서 만들어졌고, 저는 테스팅 시 ZAP에서 자주 사용합니다.

Zest in ZAP

사실 JSON 포맷 자체가 rewrite가 좋은 포맷은 아니라서(그래서 config는 yaml이나 toml을 많이 쓰죠) 직접 작성하면서 쓰기에는 좀 불편한 감이 많이 있습니다. 다만 이 Zest가 ZAP 안에서 사용하는 경우 GUI Interface를 통해 로직을 통제할 수 있기 때문에 이러한 불편함은 사라지게 됩니다.

Zest Structure

Zest는 JSON 포맷으로 스크립트의 타입, 파라미터 등을 명시할 수 있습니다. 각 값의 이름은 직관적이라 한번 쓱 보시면 어떤 역할을 하시는지 알 수 있습니다.

{
  "about": "Zest에 대한 설명",
  "zestVersion": "0.8",
  "title": "스크립트 이름",
  "description": "스크립트 설명",
  "prefix": "https://www.hahwul.com",
  "type": "StandAlone",
  "parameters": {
    "tokenStart": "{{",
    "tokenEnd": "}}",
    "tokens": {},
    "elementType": "ZestVariables"
  },
  "statements": [...],
  "authentication": [],
  "index": 0,
  "enabled": true,
  "elementType": "ZestScript"
}

Statements

Zest는 statements란 값을 기반으로 동작합니다. 이 값은 실제 처리할 액션이 담기는 값으로 Array 형태를 띕니다. Zest를 실행하면 이 Array를 순차적으로 실행하며 웹 요청을 발생하고 Action, Assertions, Contidion, Assignment에 따라서 로직을 처리하고 결과를 보여줍니다.

{
"statements": [
    {
      "url": "https://www.hahwul.com/",
      "data": "HTTP Body",
      "method": "GET",
      "headers": "Upgrade-Insecure-Requests: 1\r\nSec-Fetch-Dest: document\r\n",
      "response": {
        "url": "https://www.hahwul.com/",
        "headers": "HTTP/1.1 200 OK\r\nConnection: keep-alive\r\n\r\n",
        "body": "Response body",
        "statusCode": 200,
        "responseTimeInMs": 437,
        "elementType": "ZestResponse"
      },
      "assertions": [
        {
          "rootExpression": {
            "code": 200,
            "not": false,
            "elementType": "ZestExpressionStatusCode"
          },
          "elementType": "ZestAssertion"
        },
        {
          "rootExpression": {
            "length": 16377,
            "approx": 1,
            "variableName": "response.body",
            "not": false,
            "elementType": "ZestExpressionLength"
          },
          "elementType": "ZestAssertion"
        }
      ],
      "followRedirects": false,
      "timestamp": 1641533119265,
      "cookies": [],
      "index": 1,
      "enabled": true,
      "elementType": "ZestRequest"
    }
  ]
}

코드로는 굉장히 길어보이지만, ZAP에서 보면 좌측 메뉴와 같이 단순하게 표현됩니다.

Condition (ifStatements)

일반 언어에서 분기문(if)을 생각하시면 됩니다. Condition을 설정해서 해당 Statement에서의 Response 등에 따라서 다른 Req를 발생시키거나 결과로 표현하는 등 여러가지 액션이 가능합니다.

하나 예시를 들어보면, 제가 Response code에 따라 분기하는 Condition을 추가했습니다. THEN⇒google ELSE⇒hahwul 로 추가 요청을 하도록 구성했고, 실제로 이 Zest를 실행해보면 Response code가 200이기 때문에 아래 이미지처럼 google.com으로 추가 웹 요청이 발생합니다.

간단하죠? 단순히 이런걸 이용하면 인증 실패 시 다른 계정으로 로그인 처리라던가, 특정 플로우를 순서대로 진행하면서 자동화된 테스팅 로직을 구성할 수 있죠.

Assertions

Assertion은 검증을 위한 기능입니다. 보통 개발할 때 테스트 코드에서 Assertion 처리하는 것과 동일한 개념이죠. 우리가 지정한 룰 이외의 결과가 있다면 이를 표현해주어 알 수 있도록 제공해줍니다.

예시로 제가 Regex로 Assertion을 걸었습니다. Response에 hahwul!! 이란 글자로 정규식 매칭을 시도했고, 만약 조건에 부합하지 않는다면(매칭이 안되면) 아래 이미지처럼 X 표기와 함께 결과를 나타내줍니다.

위 과정을 모두 pseudocode로 표현하면 이런 느낌이겠죠.

r = get("https://www.hahwul.com")
if r.StateCode == 200
	r2 = get("https://www.google.com") 
	regex = /hahwul!!/g
	assert(r2.match(r.Response))
else 
	get("https://www.hahwul.com")
end

이런 형태로 Assertion 처리를 할 수 있습니다. 개인적으로 저는 Assertions를 AAA(Authentication, Authorization and Accounting) 관점으로 테스팅해야할 때 자주 사용합니다. (대량으로 권한검사할 때 진짜 편해요)

참고로 Assertions는 Response code, Regex, Length 값으로 처리할 수 있고, 이 때 Length는 일치하는 퍼센트를 지정할 수 있어서 동적으로 변하는 Response에도 어느정도 대응할 수 있습니다.

Action

Action은 실행할 액션을 지정하는 것을 의미합니다. 위에 문법과 결합하면 특정 조건일 때 Active Scan을 한다던가, 특정 변수를 세팅한다던가 할 수 있습니다.

하나 예시로 아까 THEN에서 Print하는 Action과 Sleep 3초, 그리고 기존의 다른 스크립트를 동작하는 Action 이렇게 3개를 넣어두고 구동하면 순서대로 실행됩니다.

이러면 뭐 특정 조건에서만 원하는 스크립트를 동작시키거나, ZAP에서 스캐닝을 할 수 있겠죠?

Assignment

Assignment은 변수를 컨트롤하는 기능인데요. 이를 통해서 난수를 생성하거나 특정 값을 계산하거나, 이를 form이나 특정 attribute의 value로도 쓸 수 있습니다.

하나 예시를 들어보면, 아까 THEN 구문에서 몇개 지우고 Assign으로 난수를 생성하고, Print Action으로 해당 변수 값을 찍어보겠습니다.

그러면 순서에 따라서 처리 후 Print로 인해 생성된 값을 볼 수 있습니다. 자 그럼 이걸 실제론 어떻게 쓸까요? 제가 위에선 임의로 그냥 난수값을 넣었지만, 이 변수들은 Zest 전체적으로 사용할 수 있어서 이렇게 이후 후속 요청등에서 사용할 수 있습니다.

자세히보면 www.hahwul.com으로 요청 시 하위 경로에 random 변수 값이 붙었습니다.

그래서 이걸 활용하면 CSRF Token을 자동으로 핸들링(물론 이건 AntiCSRF Token이란 기능이 있긴해요)한다거나 OAuth에서 특정 값만 사용하는 등 여러가지 방법이 있겠죠?

  참고로 변수는 {{변수명}} 형태로 사용하실  있습니다 😎

Loop

마지막으로 Loop입니다. 이름 그대로 반복문이에요. 그래서 아래 이미지처럼 String(line 단위), File, Interger 등 여러가지 패턴으로 반복문을 만들 수 있습니다.

예를들면, 로그인 페이지에 Zest를 사용하고 미리 정해둔 ID와 PW를 String으로 처리하면 여러가지 계정을 한번의 Task로 하나하나 로그인하면서 테스트할 수 있겠죠?

Zest Record using ZAP

ZAP에선 Zest record란 기능을 통해 Proxy 요청을 모두 Zest 스크립트로 포함시킬 수 있습니다. 직접 하나하나 추가하는 것보다 훨씬 빠르게 넣을 수 있어서, 저도 종종 사용하는 기능입니다 .

예전에 관련된 글을 작성했었으니, 참고하셔서 한번 써보셔도 좋을 것 같습니다.

Conclusion

이외에도 기능이 진짜 많습니다. 저도 원래 잘 안쓰다가 작년 봄부터 하나하나 익히고 실제 업무에서 사용중인데, 익숙해지면 JS, Ruby, Python 등의 스크립팅보다 훨씬 편리하게 사용할 수 있게 됩니다.

ZAP 사용자들의 대부분이 ZAP의 가장 큰 장점을 스크립팅으로 꼽고 있습니다. 자신만의 스크립트와 방식을 잘 만들어두면, 어떤 도구도 따라할 수 없는 자동화와 테스팅 플로우를 가질 수 있게됩니다.

혹시나 ZAP 유저라면 한번 Zest의 세계로 와보시는 것도 추천드려봅니다 :D