Burp Suite

노말틱 웹해킹 스터디 4주차에서는 Burp Suite의 사용법 위주로 공부했다.

Burp Suite는 클라이언트와 웹 서버 간의 통신을 분석할 수 있는 Web Proxy Tool이다.

Burp Suite는 아래 공식 홈페이지에서 다운로드 받을 수 있다. 무료 버전은 'Burp Suite Community Edition'이다.

https://portswigger.net/burp

 

Burp Suite - Application Security Testing Software

Get Burp Suite. The class-leading vulnerability scanning, penetration testing, and web app security platform. Try for free today.

portswigger.net

Burp Suite의 주요 기능들

Proxy

  •  Proxy 
    Burp Suite에서 가장 중요한 기능인 Proxy 탭이다.
  •  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.html
a.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를 얻어낼 수 있다.

'Study > with normaltic' 카테고리의 다른 글

SegFault CTF - 5주차 - 2  (1) 2025.05.06
SegFault CTF - 5주차 - 1  (0) 2025.05.06
로그인 유지: 쿠키 & 세션  (0) 2025.04.23
PHP와 MySQL  (0) 2025.04.16
간단한 로그인 페이지  (1) 2025.04.08

왜 로그인 유지를 해야하는가?

간단한 예를 통해 왜 로그인 유지가 필요한지 설명하겠다.

로그인 페이지가 있어, 로그인을 하면 해당 유저의 점수를 출력하는 사이트가 있다. 하지만 정말로 로그인이 된 상태일까? 새로고침을 해도 페이지 내용이 변하지 않는걸 보면 로그인이 된 게 맞지 않을까?

이에 대한 답은 '아니오'다. URL을 살펴보면 id의 값을 받는 것을 확인할 수 있다. 만약 이 id값을 바꿨을 때, 다른 유저의 정보 페이지가 출력된다면 이 클라이언트는 'nciwo'라는 아이디로 로그인됐다고 보긴 어렵다.

Enter id=2 without login

URL의 파라미터 값만 바꿨다고 로그인을 하지 않고도 다른 유저의 정보를 열람할 수 있다. 

이러면 로그인이 의미 없게 된다.

그렇기에 로그인을 한 뒤 해당 유저가 로그인한 ID에 해당함을 서버에서는 알 수 있어야 한다.

그것을 이 글에서는 로그인 유지라고 표현한다.

로그인 유지

그럼 어떤 방식으로 로그인을 유지시킬 수 있는가? 다른 페이지로 접속할 때마다 클라이언트로부터 ID와 PW를 받아, 로그인 인증을 한다면 클라이언트의 신원을 확인할 수 있다. 하지만 이걸 유지라고 하긴 어렵다. 상시 재확인이라는 말이 더 어울린다. 옛날 개발자들은 이러한 고민을 해결하기 위해 쿠키라는 개념을 도입했다. 로그인을 하면 서버가 클라이언트에게 특정한 키워드의 쿠키를 지급하고, 앞으로 클라이언트는 이 쿠키를 인증 도구로써 서버와 통신을 하는 것이다.

Figure 1

Figure 1에서는 로그인 과정을 개괄적으로 보여준다. 단계별로 약간의 부연설명을 붙이자면

  1. `POST id='nciwo', pw='1234'`
    맨 처음 사진에 나왔던 로그인 페이지에서, ID와 PW를 입력하고 LOGIN 버튼을 누른 상태다. 
  2. `set-cookie: login_id='nciwo'`
    해당 ID-PW에 해당하는 유저 정보를 확인한 뒤, 클라이언트에게 login_id의 값이 nciwo인 쿠키값을 전달한다.
  3. `Cookie: login_id='nciwo', GET index.php?id=nciwo`
    서버에게 login_id에 해당하는 쿠키 값으로 nciwo를 보여주며, 유저 정보 페이지를 요청한다.
  4. `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` 요청을 보내는 것을 확인할 수 있다.

그와 동시에 Cookielogin_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에서는 세션을 이용한 로그인 및 유저 페이지 요청 과정을 보여준다.

  1. `POST id='nciwo', pw='1234'`
    이 부분은 바뀌지 않는다. 그저 클라이언트가 서버에게 아이디와 비밀번호를 보내는 과정이다.
  2. `session_start -> session_id: 3289fweiaj2, uid: nciwo`
    서버에서는 세션을 시작한다. 랜덤한 이름(여기선 3289fweiaj2)의 세션 파일을 하나 만들어서 그 안에 uid라는 키의 값으로 사용자의 아이디를 적어둔다.
  3. `set-cookie: PHPSESSID=3289fweiaj2`
    세션도 인증 정보를 전달할 때는 쿠키를 사용한다. 세션파일의 이름을, 즉 세션 ID를 클라이언트에게 set-cookie로 전달한다.
  4. `GET index.php?user=nciwo, Cookie: 3289fweiaj2`
    클라이언트는 방금 받은 세션 ID를 이용해 서버에게 자신의 유저 정보 페이지를 요청한다.
  5. `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 쿠키

세션은 쿠키보다 훨씬 보안적으로 안전하지만, 서버에 파일을 생성하도록 한다. 이는 서버에 무리를 줄 수도 있다. 아무 생각없이 세션을 마구잡이로 생성하는 것은 지양해야 한다.

'Study > with normaltic' 카테고리의 다른 글

SegFault CTF - 5주차 - 1  (0) 2025.05.06
SegFault CTF - 4주차  (0) 2025.04.24
PHP와 MySQL  (0) 2025.04.16
간단한 로그인 페이지  (1) 2025.04.08
Web Server와 Web Application Server  (0) 2025.04.05

MySQL

MySQL은 Apache 재단에서 만든 대표적인 오픈소스 DBMS이다. 

PHP - MySQL 연동

PHP는 대표적인 백엔드 언어로, 당연하게도 MySQL과 연동이 가능하다.

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의 줄임말이다. PDOmysqli와는 다르게 절차지향 프로그래밍이 불가능하고, 객체지향 프로그래밍만 가능하다.

<?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` 자리에 어떠한 문자열이 와야하는지 알 수 있다. 

`"[DSN prefix]:[host];[port];[dbname];[unix_socket];[charset]"`

  • `[DSN prefix]`: MySQL은 이 자리에 `mysql`을 사용하면 된다.
  • `[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' "

조건문을 차례로 해석하면 다음과 같다.

  1. `name`이 빈 문자열이거나 '1'이 '1'과 같다 → 항상 True
  2. `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는 그런 물음표 말고도 다른 방법이 존재한다. 위와 같이 콜론 `:` 다음에 단어를 쓰면 해당 키워드를 빈 공간으로 표시할 수 있다. 

1. $stmt->bindParam("name", $id);
2. $stmt->bindParam(":name", $id);
3. $stmt->bindParam(1, $id);

세 가지 방식으로 변수를 대입할 수 있다.

  1. 아까 쓴 키워드를 첫번째 파라미터로, 변수를 두번째 파라미터로 전달하는 방법
  2. 키워드 앞에 콜론 `:`을 붙여서 명시한 키워드를 첫번째 파라미터로, 변수를 두번째 파라미터로 전달하는 방법
  3. 빈공간의 순서를 세어 첫번째 파라미터로 전달하고, 변수를 두번째 파라미터로 전달하는 방법

`bindParam()` 방식을 이용하면 값을 한 번에 하나씩밖에 전달할 수 없다. 

PDO Prepared Statements 값 여러개 한 번에 전달하기

생각보다 훨씬 쉽게 한 번에 모든 값들을 전달할 수 있다.

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=? and password=?";

    $stmt = $pdo->prepare($query);
    $stmt->execute([$id, $pw]);

    $result = $stmt->fetch();
    return $result['id'];
}

키워드 대신 `?`로 대체하고, `execute`를 실행할 때 array의 형태로 값들을 담아 넘기면 된다.

'Study > with normaltic' 카테고리의 다른 글

SegFault CTF - 5주차 - 1  (0) 2025.05.06
SegFault CTF - 4주차  (0) 2025.04.24
로그인 유지: 쿠키 & 세션  (0) 2025.04.23
간단한 로그인 페이지  (1) 2025.04.08
Web Server와 Web Application Server  (0) 2025.04.05

index.php

// index.php

<!DOCTYPE html>
<html>
    <head>
        <meta charset="UTF-8">
        <title>nciwo</title>
        <link rel="stylesheet" href="style.css">
    </head>
    <body>
        <div class="nciwo_title">
            <h1>nciwo</h1>
        </div>
        <div class="login">
            <form action="login_proc.php" method="POST">
                <div class="login-text"><p>ID</p></div>
                <input type="text" name="id">
                <div class="login-text"><p>PASSWORD</p></div>
                <input type="password" name="pw">
                <input type="submit" value="Login">
                <p id="error-message">
                <?php
                    if(!empty($_GET['login']) && !empty($_GET['n'])) {
                        if($_GET['login'] === "failed") {
                            switch($_GET['n']) {
                                case 1:
                                    echo "ID field is empty!";
                                    break;
                                case 2:
                                    echo "PASSWORD field is empty!";
                                    break;
                                case 3:
                                    echo "It's invalid ID or PASSWORD!";
                                    break;
                            }
                        }
                    }
                ?>
                </p>
            </form>
        </div>
    </body>
</html>

`<form>` 태그에서는 POST Method를 이용해 id와 pw를 login_proc.php로 넘긴다. 

아래의 php 구문은 로그인에 실패했을 시 login_php에서 파라미터를 통해 로그인 실패 이유를 받아와 띄운다.

login_proc.php

// login_proc.php

<?php
    function goBack($n) {
        header("Location: /?login=failed&n=".$n);
    }
    if(!empty($_POST['id'])) {
        if(!empty($_POST['pw'])) {
            if($_POST['id'] === "admin" && $_POST['pw'] === "admin123") {
                header("Location: home.php");
            }
            else {
                goBack(3);
            }
        }
        else {
            goBack(2);
        }
    }
    else {
        goBack(1);
    }
?>

받아온 id와 pw가 각각 "admin", "admin123"에 일치하는지 판단 후, 맞다면 home.php로, 아니라면 `goBack()` 함수를 통해 로그인 페이지로 돌려보낸다.

home.php

<html>
    <head><title>home</title></head>
    <body style="display: flex; flex-direction: column; justify-content: center; align-items: center; ">
        어 반갑고
    </body>
</html>

살갑게 인사해주는 페이지를 만나볼 수 있다.

 

 

 

스크린샷

Before typng ID & PW
After typing ID & PW
Welcome~


사이트 인테리어

/* style.css (connected to index.php) */

* {
    font-family: consolas;
}

div.nciwo_title h1 {
    color: #7b2480;
    margin: 20px 0px 0px 0px;
}

div.login {
    display: flex;
    flex-direction: column;
    justify-content: center;
    align-items: center;
    background-color: #57ba9b;
    width: 60%;
    margin-left: 20%;
    padding: 10% 8px 10% 8px;
    border-radius: 10px;
}

div.login form {
    display: flex;
    flex-direction: column;
    justify-content: center;
    align-items: center;
}

div.nciwo_title {
    display: flex;
    justify-content: center;
    align-items: center;
}

div.login input {
    border: none;
    background-color: #d9d9d9;
    border-radius: 10px;
    outline: none;
    padding: 8px;
    margin: 1px 0px 10px 5px;
    width: 150px;
}

div.login-text {
    width: 150px;
}

div.login p {
    margin: 2px 0px 0px 0px;
    color: white;
    font-size: 12px;
}

input:-webkit-autofill,
input:-webkit-autofill:hover,
input:-webkit-autofill:focus,
input:-webkit-autofill:active {
    -webkit-text-fill-color: #000;
    -webkit-box-shadow: 0 0 0px #d9d9d9 #fff inset;
    box-shadow: 0 0 0px 1000px #d9d9d9 inset;
    transition: background-color 5000s ease-in-out 0s;
}

input:autofill,
input:autofill:hover,
input:autofill:focus,
input:autofill:active {
    -webkit-text-fill-color: #000;
    -webkit-box-shadow: 0 0 0px 1000px #d9d9d9 inset;
    box-shadow: 0 0 0px 1000px #d9d9d9 inset;
    transition: background-color 5000s ease-in-out 0s;
}

div.login input[type=submit] {
    background-color: #36869e;
    margin-top: 40px;
    cursor: pointer;
    width: 220px;
    font-weight: bold;
    color: #dbdbdb;
}

div.login input[type=submit].focus {
    background-color: #246180;
}

p#error-message {
    color: #8f2828;
}

중간에 input 관련된 코드는 복붙. id field에 자동완성으로 텍스트를 채우면 css 디자인이 초기화돼버리는 상황이 발생하는 것을 막아주는 코드.

'Study > with normaltic' 카테고리의 다른 글

SegFault CTF - 5주차 - 1  (0) 2025.05.06
SegFault CTF - 4주차  (0) 2025.04.24
로그인 유지: 쿠키 & 세션  (0) 2025.04.23
PHP와 MySQL  (0) 2025.04.16
Web Server와 Web Application Server  (0) 2025.04.05

인터넷에서 우연히 점자 유니코드로 된 그림을 봤다. 재밌어 보여 만들어보기로 했다.


계획

  1. 이미지의 테두리만 남긴다.
  2. 결과물이 잘리거나 하지 않게 해상도를 맞춰준다.
  3. 4x2 픽셀들을 하나의 점자 유니코드(U+2800 ~ U+28FF)로 표현한다.

물론 계획을 하고 시작하진 않았다..


도라에몽

우리 파란 너구리를 점자로 만들어버릴 거다.

첫번째 단계

이미지의 테두리만 뽑아내는 작업이다. 여러가지 방법이 존재하지만 난 그 중에서도 직관적인 방법을 택했다.

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 1
Figure 2

원본의 `img`는 테두리가 전부 선명하다. 하지만 `blurred_img`에서는 테두리가 번지게 되면서 테두리의 픽셀 값이 원본보다 작아진다. 와중에 원래 단색으로 채워졌던 부분은 번져봤자 바로 옆에 있는 픽셀도 자신과 같은 값이였기 때문에 원본과 차이가 크게 나지 않는다.

이 상태에서 원본 `img`에서 `blurred_img`의 픽셀값을 element-wise하게 빼준다면 픽셀 값의 차이가 큰 테두리에서만 픽셀차가 0보다 크게 나와, 테두리만 뽑아낼 수 있게 되는 것이다. 

Figure 1Figure 2에서는 각 이미지 처리의 중간과정을 시각적으로 보여준다.

두번째 단계

점자 유니코드

점자 유니코드는 가로로 두 개의 점, 세로로 네 개의 점을 포함한다.

Braille Table - https://www.unicode.org/charts/PDF/U2800.pdf

위는 unicode.org에서 제공하는 U+2800부터의 문자 테이블이다. 8개의 점이 각각 색이 채워지는가 안 채워지는가에 따라 문자가 달라진다. 경우의 수는 당연하게도 `2⁸ =256`개를 갖는다.

Figure 3

점의 색이 채워지는 규칙도 당연히 정해져있다. Figure 3에 써진 숫자의 순서대로 2⁰, 2¹, 2², 2³, 2⁴, 2⁵, 2⁶, 2⁷를 U+2800에 더한 자리에 색이 채워진다. 예를 들면 다음과 같다.

U+286B

위와 같이 1, 2, 4, 6, 7번 자리가 찬 점자 문자를 얻으려면 2⁰, 2¹, 2³, 2⁵, 2⁶을 `U+2800`에 더하면 된다.

즉, ` 2⁰ + 2¹ + 2³ + 2⁵ + 2⁶ = 1 + 2 + 8 + 32 + 64 = 107`, 16진수로 `6B`를 `2800`에 더하면 `286B`가 원하던 특수문자의 번호라는 것이다.

Unicode.org - U+286B

이미지 해상도 조절

이렇게 점자 유니코드는 4x2의 크기를 갖는다. 즉, 완성될 점자 텍스트의 가로x세로 크기는 (2의 배수)x(4의 배수)가 될 것이다. 하지만 원본 이미지의 해상도가 2의 배수의 가로 크기, 4의 배수의 세로 크기를 갖지 않는다면 이미지가 잘릴 가능성이 있다. 따라서 각각 2와 4의 배수에 맞도록 이미지 해상도를 조절한다면 픽셀 정보들을 전부 담을 수 있을 것이다.

resized_shape =  round(img.shape[1] / 2) * 2, round(img.shape[0] / 4) * 4
resized_img = cv.resize(edged_img, resized_shape)

각각 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..?

..? 결과물이 너무 크다;; 해상도를 조절하는 두 번째 단계에서 약간만 코드를 추가해준다면 결과물의 크기도 변경할 수 있다.

smaller = 5

resized_shape =  round(img.shape[1] / (2 * smaller)) * 2, round(img.shape[0] / (4 * smaller)) * 4
resized_img = cv.resize(edged_img, resized_shape)

`smaller`라는 조정 변수를 도입해 크기를 좀 더 줄일 수 있다.

Broken Doraemon

해상도가 작아짐에 따라 이미지가 뭉개진다..

진짜 결과

`[이미지에서 선만 뽑아내기 → 이미지 크기 조정]` 이 과정의 순서를 `[이미지 크기 조정 → 이미지에서 선만 뽑아내기]` 이렇게 반대로 바꾸면 좀 더 좋은 결과를 얻을 수 있다.

img_path = './doraemon.jpeg'
img = cv.imread(img_path, cv.IMREAD_GRAYSCALE)

smaller = 5
resized_shape = round(img.shape[1] / (2 * smaller)) * 2, round(img.shape[0] / (4 * smaller)) * 4
resized_img = cv.resize(img, resized_shape)

edged_img = cv.subtract(resized_img, cv.pyrUp(cv.pyrDown(resized_img)))

...
### 점자로 만드는 과정 ###

Result

선이 좀 두껍긴 하지만, 도라에몽임을 인지할 정도는 된다. `threshold`의 값을 조정하거나 `smaller`의 값을 조정하면서 좀 더 나은 결과를 찾을 수도 있을 것이다.

# 최종 코드

import cv2 as cv

# Global parameter
smaller = 5
threshold = 3

# Open image file
img_path = './doraemon.jpeg'
img = cv.imread(img_path, cv.IMREAD_GRAYSCALE)

# Resize image
resized_shape = round(img.shape[1] / (2 * smaller)) * 2, round(img.shape[0] / (4 * smaller)) * 4
resized_img = cv.resize(img, resized_shape)

# Extract only edges
edged_img = cv.subtract(resized_img, cv.pyrUp(cv.pyrDown(img)))

# Find braille unicode
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)

# Make text result
result=''
for row in range(0, edged_img.shape[0], 4):
    for col in range(0, edged_img.shape[1], 2):
        result += get_number(edged_img[row:row+4, col:col+2], threshold)
    result += '\n'

print()
print(result)
# 실제 여러가지 테스트하면서 작성한 코드

import cv2 as cv
import numpy as np
from matplotlib import pyplot as plt

# Open image file
img_path = './doraemon.jpeg'
img = cv.imread(img_path, cv.IMREAD_GRAYSCALE)

smaller = 5

hehe = round(img.shape[1] / (2 * smaller)) * 2, round(img.shape[0] / (4 * smaller)) * 4
img = cv.resize(img, hehe)

# Extract only edges
edged_img = cv.subtract(img, cv.pyrUp(cv.pyrDown(img)))

blurred_img = cv.pyrUp(cv.pyrDown(img))

# shape index :: 0: row, 1: column

# resized_shape = round(img.shape[1] / (2 * smaller)) * 2, round(img.shape[0] / (4 * smaller)) * 4
# resized_img = cv.resize(edged_img, resized_shape)
resized_img = edged_img

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)

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], 4)
    result += '\n'

print()
print(f"Resolution of original image: {img.shape}")
print(f"Resolution of resized image: {resized_img.shape}")
print(result)


# plt.subplot(1,3,1)
# plt.xticks([],[])
# plt.yticks([],[])
# plt.title('img')
# plt.imshow(img, cmap='gray')

# plt.subplot(1,3,2)
# plt.imshow(blurred_img, cmap='gray')
# plt.title('blurred_img')
# plt.xticks([],[])
# plt.yticks([],[])

# plt.subplot(1,3,3)
# plt.imshow(edged_img, cmap='gray')
# plt.title('edged_img')
# plt.xticks([],[])
# plt.yticks([],[])

plt.subplot(1,2,1)
plt.imshow(edged_img, cmap='gray')
plt.title('edged_img')
plt.xticks([],[])
plt.yticks([],[])

plt.subplot(1,2,2)
plt.imshow(resized_img, cmap='gray')
plt.title('resized_img')
plt.xticks([],[])
plt.yticks([],[])


plt.show()

⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⣠⣴⡾⠿⣛⣿⡛⠛⠿⢷⣦⣄⠀⠀⠀⠀⠀⠀⠀⠀⠀
⠀⠀⠀⠀⠀⠀⠀⠀⢀⣤⣾⢟⣡⣤⣼⣿⠋⡣⢾⣿⣆⠈⠻⣷⣄⠀⠀⠀⠀⠀⠀⠀
⠀⠀⠀⠀⠀⠀⠀⠀⣾⢳⣿⣫⢷⣽⠋⠒⢱⡻⣿⡿⣃⠀⠀⠈⢻⣧⠀⠀⠀⠀⠀⠀
⠀⠀⠀⠀⠀⠀⠀⠀⢻⣾⡿⢿⣿⣾⣗⣶⣾⣿⠿⣿⣯⣭⣆⠀⠀⢿⡇⠀⠀⠀⠀⠀
⠀⠀⠀⠀⠀⠀⠀⠀⢻⣸⣧⠀⠀⠀⠈⠉⠙⠋⠛⠿⢾⣯⣿⠀⠀⢸⣿⠀⠀⠀⠀⠀
⠀⠀⠀⠀⠀⠀⠀⠀⢸⣟⣿⣆⠀⣀⣤⣤⣄⣀⡀⠀⣠⣿⡿⠀⠀⣾⡏⠀⠀⠀⠀⠀
⠀⠀⠀⠀⠀⠀⠀⠀⢀⣿⠽⠿⣆⢽⣥⣤⠈⢉⣡⣾⣿⡿⠁⠀⣼⡟⠀⠀⠀⠀⠀⠀
⠀⠀⠀⠀⢀⣴⡶⢶⣾⣟⣔⣤⣴⡎⣁⠒⠺⠿⣿⠿⠋⠀⣠⣾⠟⠀⠀⠀⠀⠀⠀⠀
⠀⠀⠀⣰⣟⣵⡿⠿⣮⢜⣸⣏⣿⡿⠀⠐⠶⣤⡄⡀⣶⡿⠛⠁⠀⠀⠀⠀⠀⠀⠀⠀
⠀⠀⠀⠻⡼⠋⠀⠀⣿⢯⣿⣄⣿⣦⣄⠀⠀⠀⠀⢸⣿⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀
⠀⠀⠀⣤⢶⣄⡀⣼⡟⠀⠙⠹⠿⠿⠽⠋⠈⠠⡔⠉⢻⡇⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀
⠀⠀⠀⠙⢿⣽⠍⠫⣶⣦⣄⡀⠀⠀⠀⠀⠀⠀⢰⣦⣻⡇⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀
⠀⠀⠀⠀⠀⠀⠀⠀⠀⠈⠙⠻⢷⣦⣄⠀⠀⠀⣸⣿⡏⣿⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀
⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠈⢻⡷⣦⡶⢿⣿⣻⡇⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀
⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠈⠻⠯⠟⠻⠵⠟⠀⠀⠀⠀⠀⠀

 

위 텍스트는 css의 `line-height` 속성을 조정하여 자연스럽게 보이도록 수정했다.

'Study > 잡다한 것들' 카테고리의 다른 글

리눅스 FTZ ssh 접속  (0) 2023.12.25

개요

사용자가 크롬과 같은 브라우저를 열어 주소창에 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`는 성립한다. 회원정보에서 비밀번호를 수정하는 과정을 예로 들어보겠다.

  1. `PUT`을 사용하는 경우
    클라이언트는 수정될 정보로 { "password": "new_password" } 를 body에 담아 전달했다. 하지만 수정된 회원 정보 결과를 보니, "password"는 원하는대로 바뀐 것을 확인할 수 있었지만 "id"는 NULL이 되어버렸다.
  2. `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

웹 서버, 웹 어플리케이션 서버 둘 다 사용하는 경우에는 클라이언트의 요청에 좀 더 능동적으로 대처할 수 있다.

요청 처리 과정

웹 서버는 정적 페이지, 웹 어플리케이션 서버는 동적 페이지를 처리한다고 상술했다. 그럼 클라이언트의 요청이 들어왔을 때 어떻게 요청을 처리하는지 단계별로 보겠다.

정적 페이지의 경우

  1. `[Client → Web Server]`: 파일 요청
  2. `[Web Server → Client]`: 파일 제공

동적 페이지의 경우

  1. `[Client → Web Server]`: 파일 요청
  2. `[Web Server → Web Container(WAS)]`: 동적페이지 요청
  3. `[WAS → Web Server]`: 동적 페이지 제공
  4. `[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는 클라이언트에게 받아온 파일을 전달한다. 또한 프록시의 기능도 있다. 요청이 자주 발생하는 파일에 대해서는 웹 서버가 갖고 있다가 클라이언트에게 바로 전달해줄 수 있다.

 

 

'Study > with normaltic' 카테고리의 다른 글

SegFault CTF - 5주차 - 1  (0) 2025.05.06
SegFault CTF - 4주차  (0) 2025.04.24
로그인 유지: 쿠키 & 세션  (0) 2025.04.23
PHP와 MySQL  (0) 2025.04.16
간단한 로그인 페이지  (1) 2025.04.08

그냥 level1에 접속하려고 하면 이렇게 "Unable to negotiate with ~ port 22: no matching cipher found."라고 뜬다.

서버와 클라이언트 간의 암호화 방식이 달라서 오류가 나는건데, 이럴 땐 -c(cipher_spec) 옵션으로 암호화 방식을 밑에 나열한 "aes128-cbc, 3des-cbc, ..." 등등의 방식으로 설정해주면 된다.

잘 접속이 된다.

하지만 여기서 문제가 끝나지 않는다.

시작하기 위해 cat으로 hint를 출력하면 한글이 깨져서 나온다.

 

Windows에서 접속할 땐 PuTTY로 접속하여 translation 옵션을 Use Font Encoding으로 설정하면 한글이 깨지지 않고 나올 수 있지만, 리눅스에서는 PuTTY를 사용할 수 없기 때문에 명령어를 추가적으로 사용해줘야 한다.

 

luit이라는 명령어에 -encoding 옵션을 넣어 eucKR로 설정해 ssh 접속을 시도한 모습이다.

luit을 통해 인코딩 방식을 eucKR로 전환하게끔 하여 ssh 접속을 하면 이렇게 깨지지 않고 정상적으로 FTZ를 이용할 수 있다.

 

 

참고 자료

https://www.x.org/archive/X11R6.8.1/doc/luit.1.html#toc0

'Study > 잡다한 것들' 카테고리의 다른 글

img2txt  (0) 2025.04.08

+ Recent posts