Post

Natas Level 27 풀이


로그인


Username: natas27
URL: http://natas27.natas.labs.overthewire.org


username 과 password 를 입력하는 폼이 있다.

소스코드를 보면,

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
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
<?php
 
// morla / 10111
// database gets cleared every 5 min
 
 
/*
CREATE TABLE `users` (
  `username` varchar(64) DEFAULT NULL,
  `password` varchar(64) DEFAULT NULL
);
*/
 
 
function checkCredentials($link,$usr,$pass){
 
    $user=mysqli_real_escape_string($link, $usr);
    $password=mysqli_real_escape_string($link, $pass);
 
    $query = "SELECT username from users where username='$user' and password='$password' ";
    $res = mysqli_query($link, $query);
    if(mysqli_num_rows($res) > 0){
        return True;
    }
    return False;
}
 
 
function validUser($link,$usr){
 
    $user=mysqli_real_escape_string($link, $usr);
 
    $query = "SELECT * from users where username='$user'";
    $res = mysqli_query($link, $query);
    if($res) {
        if(mysqli_num_rows($res) > 0) {
            return True;
        }
    }
    return False;
}
 
 
function dumpData($link,$usr){
 
    $user=mysqli_real_escape_string($link, trim($usr));
 
    $query = "SELECT * from users where username='$user'";
    $res = mysqli_query($link, $query);
    if($res) {
        if(mysqli_num_rows($res) > 0) {
            while ($row = mysqli_fetch_assoc($res)) {
                // thanks to Gobo for reporting this bug!
                //return print_r($row);
                return print_r($row,true);
            }
        }
    }
    return False;
}
 
 
function createUser($link, $usr, $pass){
 
    if($usr != trim($usr)) {
        echo "Go away hacker";
        return False;
    }
    $user=mysqli_real_escape_string($link, substr($usr, 0, 64));
    $password=mysqli_real_escape_string($link, substr($pass, 0, 64));
 
    $query = "INSERT INTO users (username,password) values ('$user','$password')";
    $res = mysqli_query($link, $query);
    if(mysqli_affected_rows($link) > 0){
        return True;
    }
    return False;
}
 
 
if(array_key_exists("username", $_REQUEST) and array_key_exists("password", $_REQUEST)) {
    $link = mysqli_connect('localhost', 'natas27', '<censored>');
    mysqli_select_db($link, 'natas27');
 
 
    if(validUser($link,$_REQUEST["username"])) {
        //user exists, check creds
        if(checkCredentials($link,$_REQUEST["username"],$_REQUEST["password"])){
            echo "Welcome " . htmlentities($_REQUEST["username"]) . "!<br>";
            echo "Here is your data:<br>";
            $data=dumpData($link,$_REQUEST["username"]);
            print htmlentities($data);
        }
        else{
            echo "Wrong password for user: " . htmlentities($_REQUEST["username"]) . "<br>";
        }
    }
    else {
        //user doesn't exist
        if(createUser($link,$_REQUEST["username"],$_REQUEST["password"])){
            echo "User " . htmlentities($_REQUEST["username"]) . " was created!";
        }
    }
 
    mysqli_close($link);
} else {
?>


먼저 주석으로 친절하게 users 테이블 구조를 알려주고 있다.

5분마다 리셋되고, username과 password는 64자리까지 쓸 수 있다.


checkCredentials($link, $usr, $pass) 함수는 입력받은 $usr, $pass 값을 mysqli_real_escape_string() 거친 후에 쿼리문 (SELECT username from users where username=’$user’ and password=’$password’)에 넣고, 해당 쿼리가 존재하면 true를 반환한다.


validUser($link, $usr)는 $usr 값을 mysqli_real_escape_string() 후에 쿼리문 (SELECT * from users where username=’$user’)에 넣고, 해당 쿼리가 존재하면 true를 반환한다.


dumpData($link, $usr) 함수는 입력받은 $usr 값을 trim()mysqli_real_escape_string()을 거친 후에 쿼리문 (SELECT * from users where username=’$user’)에 넣고, 그 쿼리가 존재하는 경우 해당 $row를 출력한다. 이 때 trim()$usr 값 양 끝에 공백이 있으면 잘라낸다.


createUser($link, $usr, $pass)는 우선 입력받은 $usr 값이 trim() 검사를 한 값과 다르면 “Go away hacker”라는 텍스트를 출력하고, 그 외엔 false를 반환한다.


trim() 검사를 통과하면, username과 password를 substr()로 최대 64자까지만 자르고, mysqli_real_escape_string()을 적용한 후, users 테이블에 INSERT 쿼리를 날린다. 이 때 INSERT가 성공하면 true를 반환하고, 실패하면 false를 반환한다.


이후 코드를 간단하게 정리하면, checkCredentials, validUser, dumpData, createUser를 이용해서
username이 이미 있으면 비밀번호를 체크하고,
없으면 새 유저를 만든다.
로그인에 성공하면 유저 데이터를 출력한다.




풀이



해당 코드의 취약점을 살펴보자.


createUser()에서 username 필드에 UNIQUE 제약 조건이 없기 때문에, 같은 username이 여러 개 존재할 수 있다는 것이다.
즉, 기존의 username과 똑같은 이름으로도 새로운 사용자가 추가될 수 있다.


또한 username과 password 모두 varchar(64)이기 때문에, 입력값이 64자를 초과하는 경우에는 자동으로 잘려(truncate) 저장된다. 이를 이용해 기존 username과 충돌(duplicate)하는 상황을 인위적으로 만들 수 있다.


그리고 username을 따로 길이 검증을 하지 않고 단순하게 mysqli_real_escape_string()만 적용하기 때문에, DB에 저장될 때 잘린 값과 로그인 시 입력값이 일치하면 인증이 가능하다.


따라서 username을 “natas28” 뒤에 57자 이상의 공백과 아무 문자열을 덧붙여 65자 이상의 긴 username을 만들고, 저장될 때 64자까지만 잘리게 만들어 기존 “natas28” 계정과 충돌시킬 수 있다.


로그인할 때도 마찬가지로 “natas28” 뒤에 공백을 붙여서 정확히 64자 username을 입력하면 DB 매칭이 성공하고,
그 이후 dumpData() 함수가 trim()을 적용하면서 기존 natas28 계정의 데이터(즉, 패스워드)를 출력하게 됨으로써 인증을 우회할 수 있다.


따라서 아래와 같은 시나리오로 패스워드를 구할 수 있었다.

  1. “natas28”+”공백 57자”+”1”로 구성된 65자의 username으로 로그인 시도하여 DB에 계정이 추가되도록 한다.

  2. “natas28”+”공백 57자”로 로그인 시도(이 때 패스워드는 위에서 로그인할 때 쓴 패스워드)

  3. 기존의 “natas28” 계정으로 착각시켜 “natas28”의 패스워드를 출력


Desktop View
1. 새로 등록할 username 길이가 65자 이상 되어야 해서... 길이재봄...


Desktop View
2. 기존의 natas28 계정과 충돌시킬 계정("natas28"+"공백57자"+"1") 생성


Desktop View
3. "natas28"+"공백57자" 를 위에서 만든 가계정 패스워드로 로그인
This post is licensed under CC BY 4.0 by the author.