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 계정의 데이터(즉, 패스워드)를 출력하게 됨으로써 인증을 우회할 수 있다.
따라서 아래와 같은 시나리오로 패스워드를 구할 수 있었다.
“natas28”+”공백 57자”+”1”로 구성된 65자의 username으로 로그인 시도하여 DB에 계정이 추가되도록 한다.
“natas28”+”공백 57자”로 로그인 시도(이 때 패스워드는 위에서 로그인할 때 쓴 패스워드)
기존의 “natas28” 계정으로 착각시켜 “natas28”의 패스워드를 출력



