Post

SQL injection 시큐어 코딩 및 실습


SQLi 시큐어 코딩



아래는 예전에 만들었었던 로그인 페이지(APM환경) 코드이다.

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
<?php
// MySQL 연결
$dbhost = 'localhost';
$dbuser = 'test22';
$dbpass = 'pass22';
$dbname = 'logintest';
$conn = mysqli_connect($dbhost, $dbuser, $dbpass, $dbname);
if (!$conn) {
  die('Could not connect: ' . mysqli_error());
}

// POST 데이터 가져오기
$username = mysqli_real_escape_string($conn, $_POST['username']);
$password = mysqli_real_escape_string($conn, $_POST['password']);

// 사용자 정보 확인
$sql = "SELECT * FROM logindata WHERE username='$username' AND password='$password'";
$result = mysqli_query($conn, $sql);

if (mysqli_num_rows($result) == 1) {
  // 로그인 성공
  session_start();
  $_SESSION['username'] = $username;
  header('Location: index.php');
  exit;
} else {
  // 로그인 실패
  echo '<script>window.alert("로그인 실패")</script>';
  include 'login.php';
}
?>


음.. 입력값을 이스케이프 하고 있기는 하지만 여러모로 취약하다.

오늘 목표는 해당 코드를 시큐어 코딩 한 후 다시 우회해보는 것…

sql injection 대응방안으로 입력값 필터링 외에도 입력값 문자열 길이 제한 등등이 있지만

가장 확실한 방안은 선처리 질의문(prepared statement)을 이용하는 것이다.

prepared statement 는 질의문을 미리 컴파일 해두기 때문에 사용자 입력값에 따라 질의문이 악의적으로 변경될 일이 없다.

이거 쓰면서 대충 생각해봤는데.. prepared statement를 다시 우회해서 공격할 방법이 있나…? 이게 되나..? 단순하게 sqli공격 단독으로는 절대 불가능하지 않나…? 아예 db 서버를 공격할 수 밖에 없지 않나…

일단 시큐어코딩부터 해보고 생각하기로 함

패스워드도 평문말고 해시로 저장하려고.. DB부터 바꿈

Desktop View
logindata 테이블에 password_hash 컬럼 추가

이후 로그인 처리하는 코드를 아래와 같이 수정했다.

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
<?php
// MySQL 연결
$mysqli = mysqli_connect('localhost', 'test22', 'pass22', 'logintest');
if (!$mysqli) {
    die('DB connect error: ' . mysqli_connect_error());
}
mysqli_set_charset($mysqli, 'utf8mb4');

// 입력값 받아오기
$username = trim($_POST['username'] ?? '');
$password = $_POST['password'] ?? '';

if ($username === '' || $password === '') {
    echo '<script>window.alert("아이디/패스워드를 입력하세요")</script>';
    include 'login.php';
    exit;
}

// prepared statement 적용
$sql  = "SELECT password_hash FROM logindata WHERE username = ? LIMIT 1";
$stmt = mysqli_prepare($mysqli, $sql);
if (!$stmt) {
    die('Prepare failed: ' . mysqli_error($mysqli));
}

mysqli_stmt_bind_param($stmt, 's', $username);
if (!mysqli_stmt_execute($stmt)) {
    die('Execute failed: ' . mysqli_error($mysqli));
}

mysqli_stmt_bind_result($stmt, $hash);
$found = mysqli_stmt_fetch($stmt);
mysqli_stmt_free_result($stmt);
mysqli_stmt_close($stmt);

$ok = false;
if ($found && !empty($hash) && password_verify($password, $hash)) {
    $ok = true;
}

if ($ok) {
    session_start();
    session_regenerate_id(true);
    $_SESSION['username'] = $username;

    mysqli_close($mysqli);
    header('Location: index.php');
    exit;
} else {
    mysqli_close($mysqli);
    echo '<script>window.alert("로그인 실패")</script>';
    include 'login.php';
}


ai의 도움을 받아 완성ㅎㅎ..
이제 웬만한 SQLi는 막히는데, 검색해보니 2차 SQLi도 있다고 한다.
회원가입할 때 1차적으로 SQL 구문을 건드릴 수 있는… 텍스트의 계정을 생성한 후
로그인할 때 2차적으로 DB 내에서 터지게끔? 하는 방식의 공격이라고 함.

진짜 세상 똑똑한 사람들 많다. 난 지식들 줏어먹으면서 감탄만 한다.

일단 자야겠음 낼 해야지 넘 피곤

2차 SQLi



3달이 지나버렸고… 본업 일이 늘어서… 주6~7일 출근해서 그랬다는 핑계를 대보고……



이전에 로그인 처리하는 페이지에만 선처리 질의문을 적용하여 웬만한 SQLi는 막히게 되었다.

그러나 입력값을 받는 모든 곳에(e.g. 회원가입 등) 적용한 게 아니라서, 2차 SQLi도 염두에 두어야 한다.


2차 SQLi의 시나리오는 간단하게 쓰면 아래와 같다.

  • 회원가입: 악의적인 SQL 구문을 포함한 username을 DB에 저장
  • 나중에 그 데이터를 사용할 때: 저장된 데이터를 prepared statement 없이 쿼리에 직접 삽입하면 공격 성공


즉 입력값을 받은 페이지에서 바로 작용하는 공격이 아니라,
취약한 페이지에서 해당 입력값을 prepared statement 없이 사용하는 경우 악의적인 쿼리가 실행되는 공격이다.


그럼 2차 sqli를 막으려면?
그냥 입력값을 받는 모든 곳에 선처리 질의문을 쓰면 된다…
또한 DB에 저장된 값을 받아올 때도 저장된 값 그대로 받아오지 말고 한 번 검사돌리고 받아오면 됨.


일단 위에서 수정한 로그인 처리 페이지는 2차 sqli에도 안전하다.

회원 db에서 username을 불러올 때 prepared statement를 사용해서 플레이스홀더(?)로 처리하기 때문에, 설령 DB에 ‘admin OR ‘1’=’1 같은 악의적인 SQL 구문이 저장되어 있더라도 그냥 문자열로만 취급된다. 즉, SQL 명령어로 해석되지 않고 단순 데이터로만 처리된다.
입력값 받는 모든 페이지에 선처리 질의문을 적용하기 전에 2차 sqli 실습도 해봐야징

This post is licensed under CC BY 4.0 by the author.