Intercept 원래는 클라이언트 - 서버 간의 통신을 지켜보고만 있지만, 이 Intercept를 켜주면, 통신을 중간에 멈추게 할 수 있다.
Open browser 프록시가 이 Burp Suite로 설정된 크로미움 브라우저를 실행한다. 굳이 쓰던 브라우저에 프록시 설정을 따로 하지 않아도 되는 편리함을 제공한다.
HTTP history
빨간 네모로 강조된 HTTP history 탭을 누르면 Burp Suite 프록시를 거쳐간 모든 HTTP 통신 기록을 볼 수 있다. 해당 기록은 google.com에 접속한 기록이다.
Repeater
Repeater 탭에서는 보냈던 패킷을 쉽게 반복해서 보낼 수 있게 해준다.
Decoder
Decoder 탭은 유틸리티 느낌의 탭이다. Encode/Decode 사이트를 들어가지 않고도 여기서 편하게 encoding/decoding을 할 수 있다.
Comparer
Comparer 탭 또한 유틸리티다. 여러 문자열들을 동시에 비교할 수 있다. 오른쪽 밑 Compare ... 에서 Words 또는 Bytes 단위로 문자열을 비교할 수 있다.
Compare Words
Compare ... 에서 Words를 눌렀을 때 나오는 창이다. 오른쪽 아래 빨간색 박스로 강조된 Sync views 를 클릭하면 스크롤을 했을 때 옆에 있는 텍스트도 같이 스크롤이 된다. 클릭하는 편이 편리할 것이다.
Segfault CTF
CTF는 Capture The Flag의 약자로, 형식이 정해진 어떠한 숨겨진 문자열을 찾아내는 일종의 게임이다. 보통은 해킹에 성공했을 때 Flag라고 불리는 문자열을 획득할 수 있다. 이번 CTF에서는 Burp Suite를 사용하면 된다는 강력한 힌트를 가지고 시작한다.
Segfault CTF
사진이 작아서 글자가 잘 보일지는 모르겠지만 Burp Suite Prac 1이라고 적힌 문제부터 풀어보자.
Burp Suite Prac 1
Burp Suite Prac 1
이러한 문구와 함께 페이지 링크를 제공해준다.
그냥 NO DATA라는 문구만 출력되는 페이지를 받았다. Burp Suite를 통해 정확히 어떤 패킷을 받았는지 확인해보자.
Request & Response
?? 풀이법을 그냥 알려준다. User-Agent 필드에 segfaultDevice를 넣어서 Request를 다시 보내보자.
이 때, Intercept를 켜고 새로고침을 해서 패킷을 다시 보내는 방법도 있고 Repeater를 통해 다시 보내는 방법이 있다. 이번에는 Intercept를 켜고 새로고침을 하여 다시 보내보겠다.
Intercept를 켜면 신호등이 빨간 불이 된다.
이제 이 빨간색 박스 부분의 값을 segfaultDevice로 변경하여 Forward 를 하면
Flag
Flag를 획득할 수 있다. segfault{모자이크} 이 부분이 Flag다.
Burp Suite Prac 2
Burp Suite Prac 2
바로 링크에 접속해보면 다음과 같은 Response를 받는다.
a.html과 b.html 두 데이터를 잘 확인하라고 한다. 링크 맨 뒤에 a.html을 붙여서 Request를 보내보겠다.
[url]/2_burp/a.htmla.html
쓸데없는 글을 반환해준다. b.html을 요청해보자.
b.html
또 Lorem ipsum이다. 하지만 사실 달라진 점이 있다. 그 점은 응답 헤더에서 확인할 수 있다. Content-Length가 7292에서 7312로 바뀌었다. 그렇다면 앞서 소개했던 Comparer 탭에서 두 문자열을 비교하면 뭐가 달라진건지 알 수 있을 것 같다.
Compare a.html and b.html
비교해보니 Flag가 b.html에 숨겨져 있었다.
Burp Suite Prac 3
Burp Suite Prac 3
드디어 멘트가 바꼈다. 무려 힌트를 제공해준다.
Response
Response를 안 주겠다고 한다. 그럼과 동시에 Set-Cookie를 통해 `answer=1`을 지정하는 것을 볼 수 있다.
F5를 누르라고 하니, 눌러보면 클라이언트에서 이러한 패킷을 전송한다.
Cookie: answer=1
content는 하나도 없고 뭔가 보낸다 싶은 거라곤 `answer=1`이라고 설정된 쿠키뿐이다.
아까 받은 힌트를 떠올려보면 1~20 사이라고 했다. 이 `answer`에 대한 정답이 1부터 20 사이의 값인 것 같다.
그럼 1부터 20까지 노가다를 해보면 될 것이다. 하지만 브라우저 들어가서 새로고침하고 다시 Burp Suite 켜서 패킷을 수정하는 과정이 번거롭다. 이럴 때 쓰는 것이 Repeater 이다.
당연하게도 답은 1이 아니다.Send to Repeater
Request에서 마우스 우클릭을 하면 Send to Repeater를 찾을 수 있다.
Repeater
이제 빨간색 박스의 answer 값을 수정하고, 파란색 박스의 Send 를 눌러 간편하게 반복적으로 패킷을 보낼 수 있다. 이걸 최대 20번만 반복하면 Flag를 얻어낼 수 있을 것이다.
1->20보다 20->1이 Flag 얻기 더 빠를 것이다.
Burp Suite Prac 4
Burp Suite Prac 4
아쉽게도 마지막 문제다.
Response
들어가자마자 쿠키를 설정하더니 나보고 Admin이 아니라고 한다. level에 대한 값으로 `dXNlcg%3D%3D`가 들어가 있다. 웬 뜬금없는 `%3D`가 들어가 있다. 이건 URL 인코딩(Percent Encoding이라고도 한다) 때문이다. 기본적으로 URL은 ASCII로 인코딩되기 때문에 한글과 같은 문자는 들어갈 수 없다(지금은 되는 것 같긴 하다만). 또한 `:`, `?`, `%`, `=`와 같은 특수문자들도 특수한 케이스에서 사용되기 때문에 이 URL 인코딩으로 변환이 된다. `%3D`는 이 중 `=`에 해당한다. 따라서 실제로 받은 level의 값은 `dXNlcg==`인 것이다. `=`가 들어간 것을 보면 어떠한 문자열이 Base64로 인코딩이 된 것 같다.
이럴 때 쓰는 것이 Decoder 이다. `dXNlcg==`를 Base64로 디코딩 해보겠다.
dXNlcg== -> user
디코딩 결과, `user`라는 단어가 나왔다.
내 level이 `admin`이 아닌 `user`였기 때문에 날 통과시켜주지 않은 것 같다. 그렇다면 `user`가 `dXNlcg==`로 인코딩된 것처럼 `admin`을 Base64로 인코딩하여 쿠키를 변조하면 통과시켜줄 것 같다.
Encode 'admin' with Base64
이 값을 쿠키에 넣어 보내보겠다. 브라우저가 알아서 `=`을 `%3D`로 인코딩해주기 때문에 이건 따로 인코딩할 필요가 없다.
Request & Response
빨간 박스를 보면 쿠키값이 위조되어 전송된 것을 볼 수 있다. 이에 대한 Response는 전과 다른 모습을 보인다. 이상한 문자열을 반환받았다.
이 문자열 또한 `=`이 들어간 것을 보아 Base64로 인코딩됐음을 직감할 수 있다.
Decoding procedure
디코딩을 하면 또 `=`가 포함된 문자열이 나오고, 또 디코딩하면 또다시 `=`가 포함된 문자열이 나와 여러번 디코딩을 반복해주면 마침내 Flag를 얻어낼 수 있다.
로그인 페이지가 있어, 로그인을 하면 해당 유저의 점수를 출력하는 사이트가 있다. 하지만 정말로 로그인이 된 상태일까? 새로고침을 해도 페이지 내용이 변하지 않는걸 보면 로그인이 된 게 맞지 않을까?
이에 대한 답은 '아니오'다. URL을 살펴보면 id의 값을 받는 것을 확인할 수 있다. 만약 이 id값을 바꿨을 때, 다른 유저의 정보 페이지가 출력된다면 이 클라이언트는 'nciwo'라는 아이디로 로그인됐다고 보긴 어렵다.
Enter id=2 without login
URL의 파라미터 값만 바꿨다고 로그인을 하지 않고도 다른 유저의 정보를 열람할 수 있다.
이러면 로그인이 의미 없게 된다.
그렇기에 로그인을 한 뒤 해당 유저가 로그인한 ID에 해당함을 서버에서는 알 수 있어야 한다.
그것을 이 글에서는 로그인 유지라고 표현한다.
로그인 유지
그럼 어떤 방식으로 로그인을 유지시킬 수 있는가? 다른 페이지로 접속할 때마다 클라이언트로부터 ID와 PW를 받아, 로그인 인증을 한다면 클라이언트의 신원을 확인할 수 있다. 하지만 이걸 유지라고 하긴 어렵다. 상시 재확인이라는 말이 더 어울린다. 옛날 개발자들은 이러한 고민을 해결하기 위해 쿠키라는 개념을 도입했다. 로그인을 하면 서버가 클라이언트에게 특정한 키워드의 쿠키를 지급하고, 앞으로 클라이언트는 이 쿠키를 인증 도구로써 서버와 통신을 하는 것이다.
Figure 1
Figure 1에서는 로그인 과정을 개괄적으로 보여준다. 단계별로 약간의 부연설명을 붙이자면
`POST id='nciwo', pw='1234'` 맨 처음 사진에 나왔던 로그인 페이지에서, ID와 PW를 입력하고 LOGIN 버튼을 누른 상태다.
`set-cookie: login_id='nciwo'` 해당 ID-PW에 해당하는 유저 정보를 확인한 뒤, 클라이언트에게 login_id의 값이 nciwo인 쿠키값을 전달한다.
`Cookie: login_id='nciwo', GET index.php?id=nciwo` 서버에게 login_id에 해당하는 쿠키 값으로 nciwo를 보여주며, 유저 정보 페이지를 요청한다.
`Return page about 'nciwo'` 서버는 클라이언트에게 해당하는 페이지를 제공한다.
cookie를 이용한 로그인
BurpSuite를 통해 실제로 통신이 이뤄지는 과정을 살펴보겠다.
POST id='nciwo', pw='1234'
로그인을 처리하는 login_proc.php로 `id=nciwo&pw=1234`를 `POST`를 통해 전달하는 모습을 확인할 수 있다. 이는 위에서 설명했던 첫번째 과정에 해당한다.
set-cookie: login_id='nciwo'
서버는 Response에서 `Set-Cookie`를 통해 login_id의 쿠키값을 nciwo로 지정해준다.
Location을 통해 클라이언트가 유저 페이지로 리다이렉션할 수 있도록 했기 때문에 클라이언트는 자동으로 유저 페이지로 `GET` 요청을 보내게 될 것이다.
Cookie: login_id='nciwo', GET index.php?id=nciwo
첫번째 줄을 통해 index.php?user=nciwo로 `GET` 요청을 보내는 것을 확인할 수 있다.
그와 동시에 Cookie에 login_id=nciwo를 넣어 서버에게 전달한다.
Return page about 'nciwo'
성공적으로 로그인했다.
만약 여기서 아까처럼 URL만 약간 수정하면 어떻게 될까? 'noot'이라는 아이디가 있다고 가정하고, user 파라미터에 nciwo 대신 넣어보자.
GET index.php?user=noot
인증에 실패했다면서 유저 정보가 뜨지 않는 것을 확인할 수 있다. 이는 파라미터로 들어온 noot과 쿠키 값인 nciwo를 일치하는지 비교한 뒤, 일치하는 경우에만 정보를 보여주도록 설계가 돼있기 때문이다.
보안이 성립된 것으로 보인다.
면 좋겠으나, 잘 생각해보면 문제점이 존재한다.
Cookie값 변조
아까 GET의 파라미터로 들어온 noot과 쿠키값인 nciwo를 비교해서 일치하면 정보를 제공한다고 했다. 여기서 문제는 보내는 쿠키값은 클라이언트가 수정할 수 있다는 점이다. 만약 쿠키값을 noot으로 변조해서 GET 요청을 다시 시도한다면 유저 noot의 정보를 가져올 수 있을지도 모른다..
일단 탈취하고자 하는 유저의 정보페이지를 URL로 입력한다.
그럼 브라우저는 서버에게 패킷을 보내려고 할 것이다. 그 패킷을 보내기 전에 변조를 시도해본다.
중요한 점은 login_id의 값을 nciwo가 아닌 noot으로 수정하는 것이다.
이제 보내보면..
로그인을 건너뛰고 다른 사용자의 정보를 가져와버렸다.
결국 쿠키는 로그인 유지를 하는 데 있어선 필요하지만 보안을 그리 강화시키지는 못한다.
이를 해결하기 위해 세션이라는 것을 사용한다.
세션
쿠키값을 id 그대로 사용하는 것 바람에 보안에 취약하다고 잠깐 생각할 수도 있다. 그렇다고 쿠키값을 랜덤하게 지정해버리면 서버 측에서는 클라이언트가 가져온 랜덤한 쿠키값이 어떤 유저에 해당하는 값인지 알 수 없다.
근본적인 원인은 인증에 사용될 값이 클라이언트에 저장돼있음에 있다. 즉, 세션은 이 근본적인 원인을 해결하기 위해 인증값을 서버에 저장하는 방식을 택한다.
Figure 2
Figure 2에서는 세션을 이용한 로그인 및 유저 페이지 요청 과정을 보여준다.
`POST id='nciwo', pw='1234'` 이 부분은 바뀌지 않는다. 그저 클라이언트가 서버에게 아이디와 비밀번호를 보내는 과정이다.
`session_start -> session_id: 3289fweiaj2, uid: nciwo` 서버에서는 세션을 시작한다. 랜덤한 이름(여기선 3289fweiaj2)의 세션 파일을 하나 만들어서 그 안에 uid라는 키의 값으로 사용자의 아이디를 적어둔다.
`set-cookie: PHPSESSID=3289fweiaj2` 세션도 인증 정보를 전달할 때는 쿠키를 사용한다. 세션파일의 이름을, 즉 세션 ID를 클라이언트에게 set-cookie로 전달한다.
`GET index.php?user=nciwo, Cookie: 3289fweiaj2` 클라이언트는 방금 받은 세션 ID를 이용해 서버에게 자신의 유저 정보 페이지를 요청한다.
`Return user page` 세션 ID에 해당하는 이름의 세션 파일을 열어보고, 그 안에 있는 uid와 요청받은 URL의 파라미터 값이 일치한지 확인 후, 클라이언트에게 페이지를 반환한다.
POST id='nciwo', pw='1234'
이번에도 똑같이 id=nciwo, pw=1234로 POST 요청을 보내본다.
set-cookie: PHPSESSID=3289fweiaj2
그럼 Response가 오는데, 이번에도 `Set-Cookie`가 있는 것을 확인할 수 있다. 하지만 이번엔 값이 약간 다르다.
PHPSESSID라는 이름을 가진 쿠키의 값으로 이상한 문자열이 들어가 있다. 저 이상한 문자열이 바로 인증에 사용될 세션 ID다.
session ID & value
실제로 세션 ID는 서버 안 특정 경로에 파일로 저장이 되며, 그 안에 사용자의 정보가 들어있다.
특히, 해당 파일의 이름이 `sess_[세션 ID]`인 것 또한 확인할 수 있다.
GET index.php?user=nciwo, Cookie: 3289fweiaj2
앞으로 클라이언트는 세션 ID를 쿠키로 설정해, 서버와 통신을 할 것이다. 서버는 해당 세션 ID에 해당하는 파일을 열어 `GET` 파라미터로 들어온 user의 값 nciwo가 일치하는지 확인한 뒤 응답한다.
Return user page
쿠키값으로 사용자 ID가 들어갈 때와 달리, 세션 ID를 유추하는 것은 사실상 불가능하기 때문에 원하는 사용자로 위장해서 접속하는 것을 막을 수 있다. 물론, 세션 ID를 탈취당한다면 위장 접속이 가능해진다.
세션 vs 쿠키
세션은 쿠키보다 훨씬 보안적으로 안전하지만, 서버에 파일을 생성하도록 한다. 이는 서버에 무리를 줄 수도 있다. 아무 생각없이 세션을 마구잡이로 생성하는 것은 지양해야 한다.
PHP와 MySQL을 연동하는 방법은 크게 두 가지가 있다. mysqli를 이용하는 방법, POD를 이용하는 방법이 있다. 각각에는 장단점이 있다.
PDO는 MySQL이 아닌 다른 DBMS와도 연동이 가능하다는 점에서 확장성이라는 장점을 가지고 있지만, MySQL의 최신 기능을 전부 지원해주지 못한다. 예를 들어, 여러 개의 query를 한 번에 보내는 Multiple Statements를 지원하지 않는다. 반면 mysqli에서는 Multiple Statements와 같은 MySQL의 다양한 기능들을 제공한다.
※ PHP 내장 라이브러리 중, mysql(mysqli 아님)이라는 라이브러리도 존재하지만 PHP 5.5.0 버전부터 사용을 권장하지 않고, 7.0.0에서부터는 삭제되었다.
◇ mysqli를 이용한 연동
mysqli는 php에 내장된 기능으로, MySQL Improved Extension의 줄임말이다. MySQL와의 연결에 최적화된 확장 라이브러리다.
C언어처럼 절차지향형 문법으로도 사용 가능하고, Java와 같이 객체지향적으로도 사용이 가능하다.
mysqli: procedural style
먼저 절차지향형 사용법을 소개하겠다.
id
name
hobby
age
1
David
Playing a game
19
2
Jay
Reading books
23
3
Kevin
Watching anime
22
예제로 사용할 데이터, `my_table`이다.
<?php
define('DB_SERVER', 'localhost'); // Make texts into constants
define('DB_USERNAME', 'username');
define('DB_PASSWORD', 'password');
define('DB_NAME', 'db_name');
$db_conn = mysqli_connect(DB_SERVER, DB_USERNAME, DB_PASSWORD, DB_NAME);
$query = "SELECT * FROM my_table";
$query_result = mysqli_query($db_conn, $query);
while($result = mysqli_fetch_array($query_result)) {
echo "<p>Name is {$result['name']}.
He(or She) likes {$result['hobby']}
and is {$result['age']} years old.</p>";
}
?>
Result
mysqli: object-oriented style
PHP는 OOP(Object-Oriented Programming)을 지원한다. mysqli또한 이에 맞게 작성된 라이브러리로, OOP 스타일로도 코딩이 가능하다.
<?php
define('DB_SERVER', 'localhost');
define('DB_USERNAME', 'username');
define('DB_PASSWORD', 'password');
define('DB_NAME', 'db_name');
$mysql = new mysqli(DB_SERVER, DB_USERNAME, DB_PASSWORD, DB_NAME);
$query = "SELECT * FROM my_table";
$query_result = $mysql->query($query);
while($result = $query_result->fetch_array()) {
echo "<p>Name is {$result['name']}.
He(or She) likes {$result['hobby']}
and is {$result['age']} years old.</p>";
}
?>
결과는 절차지향 프로그래밍으로 했을 때와 똑같다.
◆ PDO를 이용한 연동
PDO또한 PHP에 내장된 라이브러리로, PHP Data Object의 줄임말이다. PDO는 mysqli와는 다르게 절차지향 프로그래밍이 불가능하고, 객체지향 프로그래밍만 가능하다.
<?php
define('DB_USERNAME', 'username');
define('DB_PASSWORD', 'password');
$pdo = new PDO("mysql:host=localhost;dbname=db_name", DB_USERNAME, DB_PASSWORD);
$query = "SELECT * FROM my_table";
$query_result = $pdo->query($query);
while($result = $query_result->fetch()) {
echo "<p>Name is {$result['name']}.
He(or She) likes {$result['hobby']}
and is {$result['age']} years old.</p>";
}
?>
mysqli에서 객체를 만들때와 다른 점이 있다. PDO에서는 `constructor`가 받아들이는 파라미터가 약간 다르다.
좌: PDO::__construct, 우: mysqli::__construct
mysqli에서는 `$hostname`, `$username`, `$password`, `$database`를 파라미터로 두는 반면, PDO에서는 기본적으로 `$dsn`, `$username`, `$password`를 받는다.
`$username`, `$password`는 공통으로 받는 것을 확인할 수 있는데, PDO에서는 `$dsn`이란 것이 존재한다. 이 `$dsn`은 무슨 문자열일까?
DSN
DSN은 Data Source Name의 줄임말이다. PHP 공식 문서에 따르면,
The Data Source Name, or DSN, contains the information required to connect to the database.
즉, 데이터베이스에 연결하기 위한 정보들을 담는 문자열인 것이다. PDO_MYSQL DSN 공식 문서를 참고하면 이 `$dsn` 자리에 어떠한 문자열이 와야하는지 알 수 있다.
`[host]`: 연결할 MySQL이 있는 IP 주소를 쓰면 된다. 만약 자신의 컴퓨터라면 `localhost`로 대체 가능하다.
`[port]`: 연결할 IP 중에서도 포트 값이다. 이 필드를 사용하지 않는다면 기본적으로 MySQL의 기본 포트인 3306으로 시도할 것이다.
`[db_name]`: 사용할 DB의 이름이다. 이 필드를 사용하지 않고 "USE [db_name]" 쿼리를 날리는 것도 가능하다.
`[unix_socket]`: Unix Domain Socket을 사용할 때 쓰는 필드다. Unix Domain Socket은 다른 컴퓨터와의 통신이 아닌, 같은 컴퓨터 내의 다른 프로세스 간의 통신을 위한 IPC(Inter-Process Communication) 기법을 위한 소켓이다. 그러니 당연하게도 이 필드를 사용한다면 `[host]`, `[port]`와 같이 쓸 수 없다. 이 필드의 값으로는 .sock 파일의 경로를 작성하면 된다. (ex. unix_socket=/tmp/mysql.sock)
`[charset]`: 문자열 인코딩 방식을 지정해줄 수 있다.
기본적으로 필드들은 세미콜론 `;`을 통해 구분을 짓고, 맨 마지막에는 쓰지 않아도 된다. 다만, `[DSN prefix]` 뒤에는 세미콜론이 아닌 콜론 `:`이 온다는 점을 주의하자.
PDO를 이용한 코드
위에 설명한 DSN을 제외한 나머지 코드는 mysqli와 크게 다를 게 없다.
<?php
define('DB_USERNAME', 'username');
define('DB_PASSWORD', 'password');
$pdo = new PDO("mysql:host=localhost;dbname=db_name", DB_USERNAME, DB_PASSWORD);
$query = "SELECT * FROM my_table";
$query_result = $pdo->query($query);
while($result = $query_result->fetch()) {
echo "<p>Name is {$result['name']}.
He(or She) likes {$result['hobby']}
and is {$result['age']} years old.</p>";
}
?>
SQL Injection
PHP에서 쿼리를 그대로 사용하는 것은 상당히 위험하다. SQL Injection이라는 공격 기법에 취약하기 때문이다.
// login_proc.php
<?php
define('DB_SERVER', 'localhost');
define('DB_USERNAME', 'username');
define('DB_PASSWORD', 'password');
define('DB_NAME', 'loginDB');
function check_login($id, $pw) {
$db_conn = new mysqli(DB_SERVER, DB_USERNAME, DB_PASSWORD, DB_NAME);
$login_query = "SELECT * FROM user_info WHERE name='{$id}' and password='{$pw}";
$db_conn->query($db_conn, $login_query);
return mysqli_fetch_array($result)['id'];
}
?>
예를 들어 위와 같은 로그인 검사 코드가 있다고 하자. 얼핏 보기엔 문제가 없어보이지만, `$login_query`에 상당히 위험한 문제가 있다.
check_login("my_id", "password1234");
보통이라면 사용자의 입력을 받아 위와 같이 함수가 실행될 것이다. 하지만 SQL Injection을 알고 있는 사람은 위와는 다르게 함수를 호출할지도 모른다.
check_login("' or '1'='1", "' or '1'='1");
함수가 이런 파라미터를 갖고 실행된다면, `$login_query`의 값이 다음과 같이 정해진다.
" SELECT * FROM user_info WHERE name='' or '1'='1' and password='' or '1'='1' "
조건문을 차례로 해석하면 다음과 같다.
`name`이 빈 문자열이거나 '1'이 '1'과 같다 → 항상 True
`password`가 빈 문자열이 '1'이 '1'과 같다 → 항상 True
아이디와 패스워드 없이 항상 로그인에 성공을 시킬 수 있는 것이다. 이는 로그인 페이지를 완전히 무력화시킬 수 있는 공격 기법으로, 매우 위험하다.
Prepared Statements
SQL Injection은 오래된 공격 기법이고, 당연하게도 막는 방법이 존재한다. 그 중에서도 아주 간단한 방법은 Prepared Statements를 사용하는 방법이다. 문자열이 쿼리 안으로 그대로 들어가는 것을 방지해준다. PDO, mysqli 전부 이 방법을 사용할 수 있다.
mysqli Prepared Statements
function check_login($id, $pw) {
$db_conn = mysqli_connect(DB_SERVER, DB_USERNAME, DB_PASSWORD, DB_NAME);
$stmt = $db_conn->prepare("SELECT * FROM user_info WHERE name=? and password=?");
$stmt->bind_param("ss", $id, $pw);
$stmt->execute();
$result = $stmt->get_result();
return mysqli_fetch_array($result)['id'];
}
쿼리에 들어갈 변수의 자리를 `?`로 잠시 비워둔 문자열을 `$db_conn`의 `prepare` 함수의 파라미터로 넘긴다. 그 결과가 Prepared Statements가 되니, 변수로 잘 받아준다(`stmt`).
`?`로 빈 공간을 변수로 채워줄 차례다. `stmt`의 `bind_param` 함수를 이용하면 변수를 넣을 수 있다. 대신 첫번째 파라미터로 자료형을 지정해줄 필요가 있다. 두번째 파라미터부터의 자료형을 차례로 하나의 문자를 통해 정할 수 있다.
character
description
s
string 자료형
i
int 자료형
d
float 자료형
b
패킷에 담겨 보내질 BLOB 타입의 변수
사용할 자료형에 알맞는 알파벳을 순차적으로 이어붙인 문자열을 첫번째 파라미터로 넘기면 된다.
참고로 BLOB은 Binary Large OBject의 줄임말로, 이미지와 같은 큰 바이너리 파일을 저장할 때 사용되는 데이터베이스 자료형이다.
check_login("' or '1'='1", "' or '1'='1");
이제 이런 파라미터를 넣어 함수를 실행시켜도 이상을 일으키지 않음을 확인할 수 있다.
PDO Prepared Statements
<?php
define('DB_USERNAME', 'username');
define('DB_PASSWORD', 'password');
function check_login($id, $pw) {
$pdo = new PDO("mysql:host=localhost;dbname=db_name", DB_USERNAME, DB_PASSWORD);
$query = "SELECT * FROM user_info WHERE name=:name and password=:password";
$stmt = $pdo->prepare($query);
$stmt->bindParam("name", $id);
$stmt->bindParam("password", $pw);
$stmt->execute();
$result = $stmt->fetch();
return $result['id'];
}
?>
`prepare` 함수에 들어가는 쿼리문이 약간 다른 것을 확인할 수 있다. PDO를 사용할 때도 `?`로 빈 공간을 표시할 수 있다. 하지만 PDO는 그런 물음표 말고도 다른 방법이 존재한다. 위와 같이 콜론 `:` 다음에 단어를 쓰면 해당 키워드를 빈 공간으로 표시할 수 있다.
이미지의 테두리만 뽑아내는 작업이다. 여러가지 방법이 존재하지만 난 그 중에서도 직관적인 방법을 택했다.
import cv2 as cv
# Open image file
img_path = './doraemon.jpeg'
img = cv.imread(img_path, cv.IMREAD_GRAYSCALE)
# Extract only edges
blurred_img = cv.pyrUp(cv.pyrDown(img))
edged_img = cv.subtract(img, blurred_img)
그림판에서 사진의 해상도를 억지로 키우면 선명한 느낌이 사라지고 blur된 것처럼 보일 때가 있을 것이다. 이번에 사용된 edge만 남기기 방법에선 그러한 원리를 채택한다.
Figure 1Figure 2
원본의 `img`는 테두리가 전부 선명하다. 하지만 `blurred_img`에서는 테두리가 번지게 되면서 테두리의 픽셀 값이 원본보다 작아진다. 와중에 원래 단색으로 채워졌던 부분은 번져봤자 바로 옆에 있는 픽셀도 자신과 같은 값이였기 때문에 원본과 차이가 크게 나지 않는다.
이 상태에서 원본 `img`에서 `blurred_img`의 픽셀값을 element-wise하게 빼준다면 픽셀 값의 차이가 큰 테두리에서만 픽셀차가 0보다 크게 나와, 테두리만 뽑아낼 수 있게 되는 것이다.
이렇게 점자 유니코드는 4x2의 크기를 갖는다. 즉, 완성될 점자 텍스트의 가로x세로 크기는 (2의 배수)x(4의 배수)가 될 것이다. 하지만 원본 이미지의 해상도가 2의 배수의 가로 크기, 4의 배수의 세로 크기를 갖지 않는다면 이미지가 잘릴 가능성이 있다. 따라서 각각 2와 4의 배수에 맞도록 이미지 해상도를 조절한다면 픽셀 정보들을 전부 담을 수 있을 것이다.
각각 2, 4로 나눈 값을 반내림해준 뒤, 다시 2와 4를 곱해주면 원본 크기에 근접하면서도 2, 4의 배수가 되게끔 해상도를 조절할 수 있다.
Figure 4
Figure 4를 보면 해상도가 조절?되었다. 원본의 가로, 세로 크기가 원래부터 2, 4의 배수였기 때문에 크기가 바뀌지는 않았다. 아무튼 이 작업을 통해 다른 사진들 또한 깔끔하게 처리할 수 있음을 알아두자.
세번째 단계
반복문을 돌면서 4x2 크기의 픽셀들이 각각 임계값(threshold)을 넘는지를 판단해, 0 또는 1을 부여한 뒤, Figure 3에서 봤던대로 해당 4x2 픽셀들에 알맞는 점자 코드를 찾을 것이다.
def get_number(window, threshold):
ct = 0
if window[0,0] > threshold:
ct += 1
if window[1,0] > threshold:
ct += 2
if window[2, 0] > threshold:
ct += 4
if window[0, 1] > threshold:
ct += 8
if window[1, 1] > threshold:
ct += 16
if window[2, 1] > threshold:
ct += 32
if window[3, 0] > threshold:
ct += 64
if window[3, 1] > threshold:
ct += 128
return chr(0x2800 + ct)
Figure 3에 있던 점들에 위치하는 픽셀들의 값이 `threshold`보다 크다면 픽셀이 있는 것으로 판단해(1을 부여), 4x2의 픽셀 군집(코드에서 window라고 표현한다)에 알맞는 점자 유니코드를 찾아 반환하도록 한다.
뭔가 비효율적인 것 같지만 아무튼 잘 작동하는 코드다.
result=''
for row in range(0, resized_img.shape[0], 4):
for col in range(0, resized_img.shape[1], 2):
result += get_number(resized_img[row:row+4, col:col+2], 10) # 10 is threshold
result += '\n'
4x2 크기의 윈도우를 `get_number` 함수에 넘겨, 점자 문자를 받아와 `result`에 이어붙인다.
한 줄이 끝날 때마다 개행문자를 붙여 다음 줄로 넘어가도록 한다.
결과
Result..?
..? 결과물이 너무 크다;; 해상도를 조절하는 두 번째 단계에서 약간만 코드를 추가해준다면 결과물의 크기도 변경할 수 있다.
사용자가 크롬과 같은 브라우저를 열어 주소창에 www.google.com을 입력하면 구글 사이트에 접속할 수 있다.
본 글에서는 구글이 사용자에게 웹페이지를 제공하는 개괄적인 내용을 다룬다.
보통 웹페이지를 제공하는 컴퓨터를 Web Server(웹 서버)라고 부른다. 경우에 따라 Web Server가 아닌 Web Application Server를 사용할 수도, 둘 다 사용할 수도 있다.
웹 서버 (Web Server)
웹 서버는 정적 페이지를 제공한다. 대표적인 프레임워크로는 Apache, NginX가 있다.
웹페이지를 예쁘게 보여주는 것은 클라이언트에 설치된 브라우저의 역할이지, 웹서버는 그저 웹페이지를 구성하는 코드를 보내줄 뿐이다.
웹 서버에게 파일을 요청하자
정적 페이지
html, css, js와 같은 파일들로 이뤄진 페이지다. 이미 서버 컴퓨터에 저장되어 있는 파일 그대로 보내준다.
URL
URL을 이용하면 쉽게 웹서버로부터 파일을 받아올 수 있다.
URL field in Chrome
`[Protocol]://[Domain or IP Address]:[Port]/[Path]?[Parameters]`
`[Port]`와 `[Path]` 사이에 있는 `/`는 Web Root Path라고 부른다. 웹 서버가 제공하는 디렉토리 중 최상위 디렉토리라고 보면 된다.
즉, 이 Web Root Path보다 상위에 있는 디렉토리 또는 파일에는 접근할 수 없다. 가령 `../`를 사용하더라도 말이다.
Well-known Port
분명 `[Domain or IP Address]` 뒤에는 `[Port]`가 와야한다고 적혀 있다. 하지만 사진에는 https://www.google.com 뒤로 어떠한 글자도 붙어있지 않다.
바로 443이라는 포트 번호가 생략됐기 때문이다. 웹 서버는 보통 사용하는 프로토콜이 `HTTP`라면 80번 포트를, `HTTPS`라면 443번 포트를 사용한다.어차피 대부분의 서버가 정해진 포트로 사이트를 제공한다면 생략하는 편이 보기 좋다. (물론, 서버가 다른 포트를 열었다면 해당하는 포트를 명시적으로 적어줘야 문제없이 접속할 수 있다.)
사진의 경우에는 `:443`이 생략된 것이다. 실제로 `:443`을 붙이고 접속하면 구글 검색창이 정상적으로 뜰 것이다.
index.html
`[Path]`를 살펴보자. 최상위 디렉토리의 경로가 파일을 의미하는 것은 아닐 것이다. 하지만 웹페이지가 정상적으로 불러와진다. 그렇다면 `[Path]`도 생략된 걸까?
생략된 것으로 생각해도 큰 문제가 없겠지만 정확히는 생략된 것은 아니다. `[Path]`를 비워뒀을 때 웹서버에서 자동으로 제공해줄 파일이 정해져있다. 보통 이러한 파일의 이름을 index라고 한다. 대부분 HTML로 작성되므로 정확한 파일의 이름은 index.html로 생각하면 된다.
실제로 Apache 또는 NginX 같은 웹서버 프로그램을 실행하면 `/var/www/html/` 밑에 `index.html`이라는 파일을 제공하고 있는 모습을 볼 수 있을 것이다.
HTTP Method
HTTP에는 서버에 요청을 보낼 때 Method를 지정해야 한다.
대표적으로 `GET`, `POST`, `PUT`, `PATCH`, `DELETE`가 있다. 이 외에는 `HEAD`, `OPTIONS`, `CONNECT`, `TRACE`가 있다. 이 글에서는 대표적인 다섯 개의 Method만 다룬다.
`GET`: 파일을 요청하는 Method이다.
`POST`: 무언가를 등록할 때 사용하는 Method이다.
`PUT`: 데이터를 수정하는 Method이다.
`PATCH`: 마찬가지로 데이터를 수정하는 Method이다.
`DELETE`: 데이터를 삭제하는 Method이다.
멱등성 (Idempotence)
`PUT`과 `PATCH`가 당연히 똑같은 역할을 하지는 않는다. 약간의 차이가 존재한다.
멱등성이란 같은 일을 여러번 수행했을 때 결과가 같음을 의미한다. `PUT`과 `PATCH`는 이 멱등성의 특성에서 차이가 난다.
`PUT`은 멱등성이 성립하지 않는 반면, `PATCH`는 성립한다. 회원정보에서 비밀번호를 수정하는 과정을 예로 들어보겠다.
`PUT`을 사용하는 경우 클라이언트는 수정될 정보로 { "password": "new_password" } 를 body에 담아 전달했다. 하지만 수정된 회원 정보 결과를 보니, "password"는 원하는대로 바뀐 것을 확인할 수 있었지만 "id"는 NULL이 되어버렸다.
`PATCH`를 사용하는 경우 클라이언트는 수정될 정보로 { "password": "new_password" } 를 body에 담아 전달했다. 예상했던대로 "id"는 원래 값으로 유지가 되고, "password"만 수정된 것을 확인할 수 있다.
`PUT`을 사용하면 입력하지 않은 field들이 NULL 또는 Default 값으로 변경돼버릴 수 있다. 따라서 `PUT`을 사용할 땐 다른 field들의 값들도 채워줄 필요가 있다.
GET & POST
`GET`과 `POST` 둘 다 특정한 페이지를 요청하기 위해 사용할 수 있다. 당연히도, 이 둘 사이에 약간의 차이가 존재한다.
// GET 방식
<?php
<form action="login_proc" method="GET">
<input type="text" name="id">
<input type="password" name="pw">
<input type="submit" value="Login">
</form>
?>
// POST 방식
<?php
<form action="login_proc" method="POST">
<input type="text" name="id">
<input type="password" name="pw">
<input type="submit" value="Login">
</form>
?>
둘 다 login_proc으로 넘어가 로그인을 처리시키는 php 코드다. 차이점은 `GET`이냐 `POST`이냐 뿐이다. 하지만 이 다음 과정에서 URL 부분에서 차이가 크게 난다.
GET 방식POST 방식
`GET`은 URL의 `[Parameters]` field에 input을 전부 포함시킨다. 사용자의 민감한 정보가 URL에 그대로 담기는건 절대 유쾌한 일이 아니다. 따라서 `POST` 방식이 이런 보안적인 면에서는 좀 더 낫다고 볼 수 있다.
웹 어플리케이션 서버 (Web Application Server; WAS)
웹 어플리케이션 서버는 동적 페이지를 주로 제공한다. 대표적인 프레임워크로는 FastAPI, Tomcat, Spring boot 등이 있다.
동적 페이지
웹 어플리케이션 서버는 웹 서버와 다르게 동적 페이지를 제공한다. 동적 페이지는 파일을 그대로 보내주는 것이 아닌, 서버에서 클라이언트의 요청에 따라 생성된 페이지를 보내주는 페이지다. 때문에 응답 속도는 조금 더 느리지만 유연하게 제공이 가능하다.
Web Server - Web Application Server
웹 서버, 웹 어플리케이션 서버 둘 다 사용하는 경우에는 클라이언트의 요청에 좀 더 능동적으로 대처할 수 있다.
요청 처리 과정
웹 서버는 정적 페이지, 웹 어플리케이션 서버는 동적 페이지를 처리한다고 상술했다. 그럼 클라이언트의 요청이 들어왔을 때 어떻게 요청을 처리하는지 단계별로 보겠다.
정적 페이지의 경우
`[Client → Web Server]`: 파일 요청
`[Web Server → Client]`: 파일 제공
동적 페이지의 경우
`[Client → Web Server]`: 파일 요청
`[Web Server → Web Container(WAS)]`: 동적페이지 요청
`[WAS → Web Server]`: 동적 페이지 제공
`[Web Server → Client]`: 파일 제공
Web Server의 또다른 역할
웹 서버는 그냥 WAS와 함께 페이지를 제공할 수 있지만, 다른 역할로도 사용이 가능하다. WAS의 부담을 덜어주고 보안을 강화하는 Load Balancer, Reverse Proxy Server가 있다.
Load Balancer
클라이언트의 요청은 웹 서버가 받고, 웹 서버가 여러 개의 웹 어플리케이션 서버로 할 일을 나눠준다. 즉, 부하 분산의 목적이다. 하나의 웹 어플리케이션 서버에 요청이 너무 몰리지 않도록 하기 위함이다. 또한, 클라이언트와 웹 어플리케이션을 떨어뜨려 놓음으로써 보안적인 측면에서도 좋다.
Reverse Proxy Server
웹 서버는 Load balancer가 아닌 reverse proxy server의 용도로도 사용할 수 있다. Forward Proxy Server(일반적인 개념의 프록시 서버)의 서버측 버전이라고 보면 된다. 클라이언트의 요청을 받고서는 자신이 클라이언트가 된 것처럼 웹 어플리케이션 서버에 요청을 하고 파일을 받아온다. 그 뒤, Reverse Proxy Server는 클라이언트에게 받아온 파일을 전달한다. 또한 프록시의 기능도 있다. 요청이 자주 발생하는 파일에 대해서는 웹 서버가 갖고 있다가 클라이언트에게 바로 전달해줄 수 있다.