Server-side(서버 사이드) 취약점
1)정의: 서버에서는 사용자가 요청한 데이터를 해석하고 처리한 후 사용자에게 응답한다.
그 과정에서 웹 어플리케이션이나 데이터베이스와 같은 서버의 자원을 사용해 처리하기도 한다.
이 과정에서 사용자의 요청 데이터에 의해 발생하는 취약점을 서버 사이드 취약점이라고 한다.
2)발생 원인: nc나 telnet과 같은 네트워크 프로그램을 통해 HTTP Request를 보낼 때는 메소드나 요청 헤더처럼 브라우저에서 조작하지 못하는 데이터도 조작하여 전송할 수 있기 때문에
서버에서는 사용자에게 받는 모든 입력 데이터를 신뢰하지 않아야 한다.
(HTTP 요청 시 모든 데이터는 조작하여 전송이 가능하며,
악의적인 사용자는 이를 이용해 서버의 취약점을 확인 할 수 있게 된다.)
3)공격 목적: 서버 내에 존재하는 사용자들의 정보를 탈취하거나, 서버의 권한을 장악하는 등 서비스를 공격
4) 분류
-Injection:
서버의 처리 과정 중 사용자가 입력한 데이터가 처리 과정의 구조나 문법적으로 사용되어 발생하는 취약점
-File vulnerability:
서버의 파일 시스템에 사용자가 원하는 행위를 할 수 있을 때 발생하는 취약점
-Business Logic Vulnerability: 인젝션, 파일 관련 취약점들과는 다르게 정상적인 흐름을 악용하는 것을 의미
-Language specific vulnerability(PHP, Python, NodeJS)
:웹 어플리케이션에서 사용하는 언어의 특성으로 인해 발생하는 취약점
-Misconfiguration
:잘못된 설정으로 인해 발생하는 취약점을 다룸
2. [Injection]
Injection(인젝션)
1)정의: 주입이라는 의미를 가지고 있으며, 사용자의 입력 데이터가 어플리케이션의 처리 과정에서 구조나 문법적인 데이터로 해석되어 발생하는 취약점을 의미한다. 변조된 입력을 주입해 의도한 행위를 변질시켜 의도하지 않은 행위를 발생시킨다.
2) 종류
(1) SQL injection
= SQL 요청을 사용할 때 공격자의 입력 값이 정상적인 요청에 영향을 주는 취약점
(2) Command Injection
= OS Command를 사용 시 사용자의 입력 데이터에 의해 실행되는 Command를 변조할 수 있는 취약점
(3) Server Side Template Injection(SSTI)
= 템플릿 변호나 도중 사용자의 입력 데이터가 템플릿으로 사용돼 발생하는 취약점
(4) Path Traversal
= URL/ File Path를 사용 시 사용자의 입력 데이터에 의해 임의의 경로에 접근하는 취약점
(5) Server Side Request Forgery(SSRF)
=공격자가 서버에서 변조된 요청을 보낼 수 있는 취약점
<(1) SQL Injection>
웹 어플리케이션은 다양한 데이터를 관리하기 위해 DBMS를 사용한다. DBMS는 SQL이라는 질의 언어를 사용해 데이터베이스와 상호 작용 할 수 있다. 웹 어플리케이션에서 로그인/검색과 같이 사용자의 입력 데이터를 기반으로 DBMS에 저장된 정보를 조회하는 기능을 구현하기 위해
SQL 퀴리에 사용자의 입력 데이터를 추가하여 DBMS에 요청
-> 사용자의 입력이 SQL 쿼리에 삽입되어 SQL 구문으로 해석되거나 문법적으로 조작
-> 개발자가 의도한 정상적인 SQL 쿼리가 아닌 임의의 쿼리가 실행됨
즉 SQL Injection은 SQL 쿼리에 사용자의 입력 데이터가 삽입되어 사용자가 원하는 쿼리를 실행할 수 있는 취약점이다. SQL Injection을 통해
1) 현재 쿼리를 실행시키는 DBMS 계정의 권한으로 공격이 가능
2)데이터베이스의 자료를 추출하거나 삭제하는 등의 행위가 가능
<SQL(Structured Query Language)의 종류>
1) DDL(Data definition language)
데이터를 정의하기 위한 언어로, 데이터를 저장하기 위한 스키마, 데이터베이스의 생성/수정/삭제 등의 행위를 수행한다.
-CREATE: 새로운 데이터베이스 또는 테이블을 생성
CREATE TABLE Board(
idx INT AUTO_INCREMENT,
boardTitle VARCHAR(100) NOT NULL,
boardContent VARCHAR(2000) NOT NULL,
PRIMARY KEY(idx)
)
idx | boardTitle | boardContent |
)-ALTER: 데이터베이스 또는 테이블의 속성을 변경
ALTER TABLE Board ADD createdDate date;
idx | boardTitle | boardContent | createdData |
-DROP: 데이터베이스 또는 테이블을 삭제
DROP TABLE Board;
2)DML(Data manipulation language)
데이터를 조작하기 위한 언어로, 실제 데이터베이스 내에 존재하는 데이터에 대해 조회/저장/삭제 등의 행위를 수행한다.
-INSERT: 테이블에 새로운 데이터를 추가한다.
INSERT INTO
Board(boardTitle, boardContent, createdDate)
Values(
'Hello',
'World !',
Now()
);
idx | boardTitle | boardContent | createdDate |
1 | Hello | World! |
20yy-MM-dd HH:mm:ss |
-UPDATE: 테이블에 존재하는 데이터를 수정한다.
UPDATE Board SET boardContent='DreamHack!'
Where idx=1;
idx | boardTitle | boardContent | createdDate |
1 | Hello | DreamHack! |
20yy-MM-dd HH:mm:ss |
-SELECT: 테이블에 존재하는 데이터를 조회한다.
SELECT
boardTitle, boardContent
FROM
Board
Where
idx=1;
<얻게 되는 결과값>
Hello, DreamHack!
-DELETE: 테이블에 존재하는 데이터를 삭제한다.
DELETE From
Board
Where
idx=1;
idx | boardTitle | boardContent | createdDate |
3)DCL(Data control language)
데이터베이스의 접근 권한 등의 설정을 하기위한 언어로, 데이터베이스내에 사용자의 사용 권한을 부여하기 위한 GRANT와 권한을 박탈하는 REVOKE가 대표적인 DCL이다.
SQL Injection
사용자의 입력 데이터가 SQL 쿼리에 들어가는 대표적인 예시= 로그인 기능
사용자가 아이디와 패스워드를 입력해 서버에 전송
->서버는 해당 데이터가 데이터베이스에 존재하는지 확인, 로그인을 성공시킬지 실패시킬지 판단
<MYSQL 에서의 로그인을 처리하는 가장 간단한 형태의 쿼리>
select * from user_table
where uid='{uid}' and upw='{upw}';
->사용자가 자신의 아이디와 패스워드를 입력하면 웹 어플리케이션에서 {uid}, {upw} 부분에 사용자가 입력한 문자열로 대체되어 들어가고 DBMS로 전달되어 실행한다.
위 SQL 쿼리는 사용자의 입력과 웹 어플리케이션이 작성한 SQL 쿼리를 해석할 때 '를 기준으로 문자열을 구분한다. 만약 사용자가 입력에 '를 포함시켜 문자열을 탈출하고 뒷 부분에 새로운 쿼리를 작성하여 전달하면 DBMS에서 사용자가 입력한 쿼리를 실행시킬 수 있다.
idx | uid | upw |
1 | guest | guest |
2 | admin | *********** |
위와 같은 테이블 구조에서
1) uid= guest와 upw=guest데이터를 입력하면, uid와 upw의 값이 모두 동일하여
테이블 내에 존재하는 guest 정보를 출력한다.
2)admin의 upw를 알고 있지 않을 때 SQL Injection을 통해 논리적으로 참이 되는 구조를 만들어
admin의 정보가 출력되어지게 할 수 있다.
-> or 연산(A or B와 같은 구조일 경우 A또는 B중 하나라도 참인 경우 결과가 참)을 이용
-> 1' or '1
<SQL Injection을 막는 방법>
사용자의 입력 데이터가 SQL 쿼리로 해석되지 않아야 한다.
앞서 살펴본 경우는 사용자의 입력 값에 문자열 구분자(',")가 들어있어 원래 쿼리 형태를 벗어나 SQL Injection이 발생했기 때문에 이를 막아야 한다.
과거에는 문자열 구분자 앞에 백슬래시(\)를 붙여 사용자의 입력을 escape해 사용하는 방식을 자주 이용했지만 사용자로부터 입력을 받는 타입은 아이디, 패스워드와 같이 문자열일수도 있지만 숫자 타입일 수도 있기 때문에 최근에는 권장x
-> 문자열 구분자를 모두 escape 했더라도 숫자 뒤에 공백문자(' ', \n, ..)를 넣는 것 만으로도 바로 쿼리에 사용자 입력을 삽입할 수 있기 때문에 SQL Injection이 발생할 수 있음
->문자열 구분자를 삽입하지 않아도 where절의 조건을 항상 참이 되도록 만들 수 있다.
(예시) select * from board_table where post_idx=100 or 1=1;
ORM(Object Relational Mapper)과 같이 검증된 SQL 라이브러리를 사용
-> 개발자가 직접 쿼리를 작성하는 Raw 쿼리를 사용하지 않아도 기능 구현이 가능하여, SQL injection으로부터 상대적으로 안전: 사용자의 입력 값을 라이브러리 단에서 알아서 escape하고 쿼리에 매핑시키기 때문
※ORM을 사용하더라도 입력 데이터의 타입 검증이 없으면 잠재적인 위협이 될 수 있기 때문에
입력 데이터의 타입 검증이 필요하다.
<ORM Example(python SQLAlchemy)>
from flask_sqlalchemy import SQLAlchemy
...
db = SQLAlchemy(...)
...
class User(db.Model):
id = db.Column(db.Integer, primary_key=True)
uid = db.Column(...)
upw = db.Column(...)
...
User.query.filter(User.uid == uid, User.upw == upw).all()
<(2) Command Injection>
웹 어플리케이션에서 OS Command를 사용하기 위해 PHP(system), Node JS(child_process), Python(os.system)과 같이 OS Command를 실행하는 함수가 구현되어 있다. OS Command란 linux(ls, pwd, ping,zip), winodws(dir,pwd,ping)등 OS에서 사용되는 Command이다.
OS Command를 사용하는 이유: 이미 기능을 구현한 OS 실행 파일이 존재할 때 코드 상에서 다시 구현하지 않고 OS command로 실행하면 더 편리하기 떄문
OS Command는 내부적으로 쉘을 이용해 실행하는데, 쉘에는 한 줄에 여러 명령어를 실행하는 등의 쉘 사용자 편의성을 위해 제공하는 특수문자들이 존재한다. OS Command를 사용할 때 만약 사용자의 인풋이 검증되지 않고 그대로 OS Command 함수에 들어가게 된다면 특수 문자를 이용해 사용자가 원하는 명령어를 함께 실행하게 될 수도 있다.
<특수문자의 종류>
1)' ' (명령어 치환)
' ' 안에 들어있는 명령어를 실행한 결과로 실행된다.
$ echo 'echo theori'
theori
2)$() (명령어 치환)
$() 안에 들어있는 명령어를 실행한 결과로 치환된다. 이 문자는 위와 다르게 중복 사용 가능
$ echo $(echo $(echo theori))
3)&& (명령어 연속 실행)
한 줄에 여러 명령어를 사용하고 싶을 때 사용한다. 앞 명령어에서 에러가 발생하지 않아야 뒷 명령어를 실행한다.
(Logical And)
$ echo hello && echo theori
hello theori
4)|| (명령어 연속 실행)
한 줄에 여러 명령어를 사용하고 싶을 때 사용한다. 앞 명령어에서 에러가 발생해야 뒷 명령어를 실행한다.
(Logical Or)
$ cat / || echo theori
cat: /: Is a directory
theori
5); (명령어 구분자)
한 줄에 여러 명령어를 사용하고 싶을 때 사용한다. ;는 단순히 명령어를 구분하기 위해 사용하며, 앞 명령어의 에러 유무와 관계 없이 뒷 명령어를 실행한다.
$ echo hello ; echo theori
hello
theori
6)| (파이프)
앞 명령어의 결과가 뒷 명령어의 입력으로 들어간다.
$echo id | /bin/sh
uid=1001(theori) gid=1001(theori) groups=1001(theori)
<Command Injection을 막는 방법>
사용자의 입력 데이터가 Command 인자가 아닌 다른 값으로 해석되는 것을 막아야 한다.
-> 가장 좋은 방법은 웹 어플리케이션에서 OS Command를 사용하지 않는 것(OS Command를 사용할 경우
해당 Command 내부에서 다른 취약점이 발생하는 등 잠재적인 위협이 될 수 있기 때문)
(OS Command가 라이브러리 형태로 구현되어 있는 경우 = 해당 라이브러리 사용
OS Command가 라이브러리 형태로 구현되어 있지 않은 경우= 직접 프로그램 코드로 포팅)
그럼에도 OS Command에 사용자의 입력 데이터를 넣어 사용해야할 경우
->필터링을 통해 Command Injection을 방지
1) 정규식을 통한 화이트리스트방식 필터링
ex) ping을 보내는 페이지의 경우 사용자가 입력한 ip address가 정상적인 ip address 형식인지 정규식으로 검증 후 사용할 수 있다.
import re, os, ...
...
chk_ip = re.compile('^((25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)\.){3}(25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)$')
if bool(chk_ip.match(ip)):
return run_system(f'ping -c 3 {ip}')
else:
return 'ip format error'
2) OS Command에서 Meta 문자로 사용되는 값을 필터링하고 따옴표로 감싸기
ex) ping을 보내는 페이지의 경우 사용자가 입력한 ip address를 따옴표로 감싸서 사용할 수 있다.
※Double quotes(")를 사용할 경우 dollarsign($), backquote(')가 해석되어 모든 입력을 문자열로 처리하는 single quotes(')를 사용해야 한다.
if ip in '\'':
return 'not allowed character'
return run_system(f'ping -c 3 \'{ip}\'')
3) execve args 인자로 사용
shell meta 문자로 해석되지 않게 입력 값을 넣는다.
subprocess.Popen(['ping', '-c', '3', ip]) # B
4) 기능에 해당하는 라이브러리 사용
사용하고자 하는 기능을 OS 커맨드가 아닌 구현한 라이브러리로 대체 사용 가능
(※라이브러리의 보안성 및 안정성 등을 검토한 후 사용해야 한다.)
#! pip install ping3
# https://github.com/kyan001/ping3/blob/master/ping3.py
import ping3
ping3.ping(ip)
//ping3은 소켓 프로그래밍을 통해 ping 기능을 구현한 라이브러리
<(3)Server Side Template Injection(SSTI)>
웹 어플리케이션에서 동적인 내용을 HTML로 출력할 때 미리 정의한 Template에 동적인 값을 넣어 출력하는 Template Engine을 사용하기도 한다.
만약 (template 내부에서 사용되는 context가 아닌) template source에 사용자 입력이 들어가게 된다면 악의적인 입력을 통해 개발자가 의도하지 않은 임의의 template 기능을 실행할 수 있다. 즉, 사용자의 입력 데이터가 template에 직접 사용될 경우 template engine이 해석하여 실행하는 문법을 사용할 수 있기 때문에 SSTI 취약점이 발생.
SSTI 취약점을 막기 위해서는= 사용자의 입력 데이터를 template source에 삽입되지 않도록 해야한다. 사용자의 입력 데이터를 template에서 출력하기 위해서는 template context에 값을 넣어 출력해야 한다.
<각 언어별 많이 사용되는 Template Engine>
Language |
Template Engine |
Pythoon | Jinia2, Mako, Tornado.. |
PHP | Smarty, Twig, .. |
JavaScript | Pug, Marko, EJS.. |
<(4) Path Traversal>
1) path traversal 취약점 발생 원인: 사용자의 입력 데이터가 적절한 검증 없이 URL/File Path에 직접적으로 사용될 경우 설계 및 개발 당시에 의도하지 않은 임의의 경로에 접근할 수 있는 Path Traversal 취약점이 발생.
ex)
내부 API가 path variable로 입력 데이터를 받는 형식으로 구현되어있어 입력 데이터가 url path로 들어가는 경우
dream이라는 유저의 정보를 가져오기 위해
http://internal.dreamhack.io/api/user/dream 과 같이 사용자 입력이 url에 들어갈 때
URL 구분 문자를 사용하지 못하도록 하는 필터링 또는 인코딩 없이 사용하게 된다면 ../과 같은 구분 문자를 통해
의도한 경로가 아닌 상위 경로에 접근해 다른 api를 호출 할 수 있다.
<URL에서 해석되는 구분 문자>
1) / = path identifier
2).. = path directory
*/tmp/test/../1234 -> /tmp/1234
3) ? = query identifier
* ? 뒤는 query로 해석
4) # = Fragment identifier
* # 뒤의 값은 server로 전달되지 않음
5) & = parameter identifier
* key1=value&key2=value.. 형식으로 사용
<(5) Server-side Request Forgery(SSRF)>
1)SSRF : server-side에서 변조된 요청/ 의도하지 않은 서버로 요청을 보내는 공격
웹 어플리케이션에서 사용자가 입력한 URL에 요청을 보내는 기능이 구현되어야 하는 경우에 발생
2)CSRF(Cross-site Request Forgery)와의 차이점:
CSRF= 변조된 요청을 웹 클라이언트(브라우저)가 보낸다.
SSRF= 변조된 요청을 웹 어플리케이션에서 보낸다.
웹 어플리케이션에서 요청을 보내기 때문에 웹 어플리케이션이 작동하고 있는 서버 내부의 포트, 서버와 연결된 내부망에 요청을 보낼 수 있다.
3)SSRF 취약점을 방지하기 위한 방법:
<1> 사용자가 입력한 URL의 Host를 화이트리스트방식으로 검증-
(1)미리 신뢰할 수 있는 Domain Name, IP Address를 화이트리스트에 등록
(2) 사용자가 입력한 URL에서 Host 부분을 파싱해 화이트리스트에 있는지 확인하여 사용
URL Host 화이트리스트방식 필터링
from urllib.parse import urlparse
WHITELIST_URL = [
'i.imgur.com',
'img.dreamhack.io',
...
]
SCHEME = ['http', 'https']
def is_safehost(url):
urlp = urlparse(url)
if not urlp.scheme in SCHEME:
return False
hostname = urlp.hostname.lower()
if hostname in WHITELIST_URL:
return True
return False
print(is_safehost('https://127.0.0.1/'))
print(is_safehost('https://i.imgur.com/Bsz7RJN.png'))
<2>사용자가 입력한 URL의 Host가 내부망/루프백 주소인지 블랙리스트방식으로 검증-
->http://127.0.0.4/, http://0x7f000001/ 와 같은 다양한 루프백 주소 사용
->Host에 Domain Name을 넣어 DNS Rebinding 공격 등으로 우회
<3>사용자의 URL을 처리하는 서버를 독립적으로 망분리하여 SSRF 취약점이 발생하여도 다른 취약점과 연계를 하지 못하도록 방지
3. File Vulnerability
파일을 업로드/다운로드 하는 기능에서 발생할 수 있는 취약점들과 취약점으로부터 안저하게 구현할 수 있는 방법들에 대해 알아본다.
1) File Upload= 서버의 파일 시스템에 사용자가 원하는 경로 또는 파일명 등으로 업로드가 가능하여 악영향을 미칠 수 있는 파일이 업로드 되는 취약점
<취약점이 발생하는 이유>
사용자의 파일이 서버의 파일 시스템에 저장되어 처리되기 때문
사용자가 파일 업로드 시 파일의 내용과 함꼐 파일의 이름도 함께 서버로 전송 되는데, 이 때 서버가 아무런 검증이 없다면 서버의 파일시스템에 원하는 파일을 저장할 수 있음
<--HTTP Request-->
POST /fileUpload HTTP/1.1
Host: storage.dreamhack.io
Origin: https://storage.dreamhack.io
Content-Type: multipart/form-data; boundary=----WebKitFormBoundary20y3eoLVSNf9Ns5i
------WebKitFormBoundary20y3eoLVSNf9Ns5i
Content-Disposition: form-data; name="file"; filename="test.txt"
Content-Type: text/plain
upload test !
------WebKitFormBoundary20y3eoLVSNf9Ns5i--
<--Server File System-->
$ ls -lR
-rw-r--r-- 1 dreamhack staff 461 1 30 21:52 app.py
drwxr-xr-x 3 dreamhack staff 96 1 30 21:31 uploads
./uploads:
total 8
-rw-r--r-- 1 dreamhack staff 13 1 30 21:31 test.txt
<--HTTP Request-->
POST /fileUpload HTTP/1.1
Host: storage.dreamhack.io
Origin: https://storage.dreamhack.io
Content-Type: multipart/form-data; boundary=----WebKitFormBoundary20y3eoLVSNf9Ns5i
------WebKitFormBoundary20y3eoLVSNf9Ns5i
Content-Disposition: form-data; name="file"; filename="../hack.py"
Content-Type: text/plain
[malicious file content]
------WebKitFormBoundary20y3eoLVSNf9Ns5i--
<--Server File System>
$ ls -lR
-rw-r--r-- 1 dreamhack staff 461 1 30 21:52 app.py
-rw-r--r-- 1 dreamhack staff 431 1 30 22:12 hack.py
drwxr-xr-x 3 dreamhack staff 96 1 30 21:31 uploads
./uploads:
total 8
-rw-r--r-- 1 dreamhack staff 13 1 30 21:31 test.txt
<File Upload를 통한 공격자의 공격방식>
파일 업로드 취약점을 통해 서버 파일 시스템에 원하는 파일 경로 또는 파일 명을 업로드 할 수 있다면
-> 악의적인 공격자는 웹 어플리케이션 또는 서버의 서비스가 참조하는 파일을 업로드하여 공격에 사용할 수 있음
cf) CGI(Common Gateway Interface): 사용자의 요청을 받은 서버가 동적인 페이지를 구성하기 위해 엔진에 요청을 보내고 엔진이 처리한 결과를 서버에게 반환하는 기능
ex) 사용자가 apache 서버에 요청을 보내면, apache는 사용자의 요청을 해석
사용자가 요청하는 리소스의 확장자가 .php와 같이 php 엔진을 사용하기로 설정되어 있다면,
mod_php(CGI)를 통해 사용자의 요청을 php엔진이 처리 및 실행하도록 요청한다.
즉, php, jsp,asp 등과 같이 CGI를 통해 서비스를 하는 형태에서는 확장자를 통해 웹 어플리케이션 엔진에 요청 여부를 판단한다. .php 확장자 외에도 .php3/4/5/7, .pht, .phtm1 등의 확장자를 가진 파일이라면 php에서 처리되도록 핸들링 되어 있는 것을 확인 할 수 있다.
cf) python의 flask, django와 nodejs의 express등은 서버,CGI, 웹 어플리케이션 등이 통합된 프레임워크로, 미리 라우팅 된 경로에만 접근이 가능하도록 설정하여 서비스 되어진다.
사용자의 요청-> 웹 서버가 파일 시스템에서 해당 파일을 찾아 실행-> 서버가 엔진에 확장자 요청
Webshell(웹쉘)= 웹 어플리케이션이 실행하는 코드를 악의적인 공격자가 조작할 수 있다면 웹 어플리케이션 언어에 내장된 OS 명령어 등을 사용할 수 있으며, 해당 서버와 쉘을 웹을 통해 사용한다고 하여 웹쉘 이라는 악성코드가 등장
2)File Download 취약점: 서버의 기능 구현상 의도하지 않은 파일을 다운로드 할 수 있는 취약점
사용자가 입력한 파일이름을 검증하지 않은 채 그대로 다운로드 시켜줄 때 흔히 발생한다.
<취약한 코드 예시>
...
@app.route("/download")
def download():
filename = request.args.get("filename", "")
return open("uploads/" + filename, "rb").read()
...
개발자는 사용자가 uploads 디렉토리 안에 존재하는 파일만 다운로드 할 수 있을 것이라 생각할 수 있지만, injection 주제에서 배운 path traversal을 이용하면 uploads 경로보다 더 상위 경로에 존재하는 시스템 파일, 설정 파일과 같은 중요한 정보들을 다운로드 할 수 있다.
ex)
정상경로) http://example.com/download?filename=docs.pdf
공격자 경로) http://example.com/download?filename=../../../../etc/passwd
파일 다운로드 취약점은 웹 어플리케이션의 소스코드, 관리자의 패스워드, 서비스 키, 설정 파일 들을 유출할 수 있기 때문에 주로 이차적인 공격을 위한 발판으로 삼는데에 많이 쓰인다.
<파일 다운로드 취약점을 막기 위한 방법>
-기본적으로 인자에 다운로드 받으려는 파일의 경로나 이름을 넘기지 않는 것이 좋다.
-반드시 이름을 넘기는 방식으로 구현해야 한다면 상대경로로 올라가는 데 사용될 수 있는 ..과 / 와 \\를 적절하게 필터링 해야 한다.
-데이터베이스에 다운로드 될 파일의 경로와 그에 해당하는 랜텀 키를 생성해 1대 1로 매칭해서 저장해두고 해당 랜덤 값이 인자로 넘어왔을 때 데이터베이스에 존재하는 파일인지를 먼저 식별하고 다운로드 한다.
@app.route("/download")
def download():
file_id = request.args.get("file_id", "") # file_id는 쉽게 유추하지 못하는 랜덤한 값이어야 합니다.
file_path = find_path_from_database(file_id) # find_path_from_database 함수는 데이터베이스에서 file_id와 매핑된 파일 경로를 반환하는 개발자가 작성한 함수입니다.
if file_path is None:
return "올바르지 않은 파일 아이디입니다."
return open(file_path, "rb").read()
<파일 다운로드 취약점을 막기 위해 잘못 구현한 경우>
다운받으려는 파일 경로에서 단순히 ../만을 필터링 하려고 filename = filename.replace("../","")과 같이 상위 경로로 올라가는 키워드를 없애면 상위 경로로 못 올라가기 때문에 안전할 것이라고 생각할 수 있으나
-> 공격자는 ..././file과 같은 형식으로 요청해 ../가 삭제되어 다시 새로운 ../를 만드는 형식으로 우회 할 수 있다.
또한 만약에 웹 서버가 동작하는 운영체제가 윈도우 운영체제일 경우 ../ 외에 ..\\로도 상위 경로에 접근할 수 있기 때문에 둘다 필터링 하는 것이 필요하다.
4. Business Logic Vulnerability
Business logic(비즈니스 로직)= 규칙에 따라 데이터를 생성, 표시, 저장, 변경하는 로직, 알고리즘 등을 말한다.
예를 들어 게시판 서비스는 회원 가입/로그인, 게시물 작성/수정/삭제 등 다양한 로직이 모여 하나의 서비스가 완성
Business logic vulnerability(비즈니스 로직 취약점)= 정상적인 비즈니스 로직을 악용하는 것을 의미. 인젝션, 파일 관련 취약점들은 사용자의 악의적인 데이터가 서버의 시스템 상에서 악영향을 미치는 공격을 수행하지만, 비즈니스 로직 취약점은 서비스의 기능에서 적용되어야 할 로직이 없거나 잘못 설계된 경우 발생하게 된다.
<게시물 수정을 위한 비즈니스 로직 예시>
1. 사용자가 게시물 수정을 요청
2. 로그인된 사용자인지 확인
3. 수정을 요청한 사용자가 해당 게시물을 수정 할 수 있는 권한인지 확인
4. 2,3번 과정이 확인되면 데이터베이스에 사용자가 입력한 정보로 수정
-> 위와 같은 상황에서 3번 과정이 설계 과정의 실수로 인해 누락될 경우 악의적인 공격자는 다른 사용자의 게시물도 수정할 수 있는 비즈니스 로직 취약점이 발생하게 된다.
<비즈니스 로직 취약점 종류>
1)Business Logic Vulnerability
정상적인 흐름에서 검증 과정의 부재 및 미흡으로 인해 정상적인 흐름이 악용되는 취약점
2)IDOR(Insecure Direct Object Reference)
변조된 파라미터 값이 다른 사용자의 오브젝트 값을 참조할 때 발생하는 취약점
3)Race Condition
비즈니스 로직의 순서가 잘못되거나, 한 오브젝트에 여러 요청이 동시에 처리되는 상황에서 발생하는 취약점
(1) Business Logic Vulnerability
발생 원인) 어플리케이션의 검증 부재 또는 미흡의 이유로 발생
예시) 후기 작성 시 마다 100포인트를 지급하는데 후기를 작성하고 삭제하는 행위를 반복해도 포인트를 차감하지 않아 무제한으로 적립받을 수 있는 비즈니스 로직 취약점
또 다른 문제점) 사용자가 후기 이벤트를 통해 얻은 포인트를 쇼핑 또는 송금과 같이 포인트를 사용하는 다른 기능에서 모든 포인트를 사용한 후 후기를 삭제하게 되면 다른 취약점이 발생할 수도 있음
해결방안) 후기 삭제로 인해 다른 취약점이 발생하는 것을 방지하기 위해 후기 삭제 기능 지원x
후기 삭제 기능을 허용하기 위해 최초 1회에만 포인트 지급
@app.route('/reviewWrite')
def review_write():
userName = session['username']
contents = request.form['contents']
if review_Check(userName):
return "Already write."
result = review_Insert(userName, contents)
pointResult = userPoint(userName, 100)
#pointResult = userPoint(userName, -100)이 추가되어야 함
if result and pointResult:
return "write success."
else:
return "write fail."
(2) IDOR(Insecure Direct Object Reference)= 안전하지 않은 객체 참조
정의) 객체 참조 시 사용하는 객체 참조 키가 사용자에 의해 조작됐을 때 조작된 객체 참조 키를 통해 객체를 참조하고, 해당 객체 정보를 기반으로 로직이 수행되는 것을 의미한다.
발생원인) 사용자의 입력 데이터에 의해 참조하는 객체가 변하는 기능에서 사용자가 참조하고자 하는 객체에 대한 권한 검증이 올바르지 않아 발생한다.
사례) IDOR 취약점은 비즈니스 로직에 따라 조회/삭제/수정/추가 등의 다양한 형태를 다른 사용자의 객체로 수행할 수 있다.
취약점을 막기 위한 방법)
-객체 참조 시 사용자의 권한을 검증
즉, 사용자가 의도한 권한을 벗어나서 행동할 수 없도록 권한 등을 분리해서 관리
-사용자 식별을 위한 정보를 사용자의 입력 데이터로 구분하기 보다는 사용자가 요청 시 전달하는 세션을 통해 서버 내에서 처리
-객체 참조 키를 단순한 숫자가 아닌 무작위 문자 생성 등을 통해 추측하기 어려운 객체 참조 키 생성
cf) 숫자만을 이용한 해쉬 함수로 객체 참조 키를 사용시
-> 레인보우 테이블에 의해 사실상 평문으로 사용하는 것과 동일
해쉬 함수를 이용해 객체 참조 키를 사용할 경우
-> 솔트(salt)를 사용해 해쉬 함수를 안전하게 사용해야 함
(3) Race Condition(레이스 컨디션)
정의) 공유 자원 처리 과정에서 해당 자원에 대한 동시 다발적인 접근으로 인해 발생하는 취약점
발생원인) 데이터를 참조하는 타이밍의 차이로 인해 취약점 발생
주로 검증 과정에서의 데이터와 수정 과정의 데이터의 차이로 인해
웹 어플리케이션에서 의도하지 않은 흐름으로 진행& 비즈니스 로직의 잘못된 순서로 인해 취약점이 발생하기도 함
사례) 사용자의 자원인 잔액을 확인하는 과정과 차감하는 과정 사이인 재고 확인 과정에서 시간 지연이 발생하며, 잔액 확인 과정 이후에 잔액이 차감된다는 점을 이용하여 현재 잔액보다 더 많은 금액의 상품을 구매할 수 있음
취약점을 막기 위한 방법)
-레이스 컨디션 취약점으로부터 안전하게 보호되어야하는 로직들은 하나의 접근이 끝난 후 다음 접근을 처리하도록 쓰레드 락 등을 통해 동시 다발적인 접근을 방지해야 함
-레이스 컨디션 취약점을 발생시키기 위해 다량의 접근이 필요하다는 점을 이용하여 특정한 행위에 요청을 할 시에는 CSRF 토큰, 캡차 등과 같은 다량의 접근을 방지하는 방법을 통해 웹 어플리케이션 보호
5. Language specific Vulnerability
해당 언어만의 특성, 함수에서 발생할 수 있는 취약점
ex) 의도한 기능을 제공하는 함수를 잘못 사용해서 문제 발생, 사용자의 입력 데이터가 함수의 인자로 사용되어 취약점 발생
<종류>
1)Common = 코드 실행 함수(eval), OS command function, Filesystem function, serialize/deserialize
2)PHP = include, Wrappers, extract, Type Juggling, Comparison, session, Upload logic
3)Javascript = comparison problem, prototype pollution
(cf. 자바스크립트는 클라이언트 사이드에서는 웹 브라우저의 내장 언어로 사용되며, 서버 사이드에서는 자바스크립트를 기반으로 만들어진 NodeJS로 사용될 수도 있다.
# eval= 인자로 입력된 문자열을 어플리케이션 코드로 실행
eval의 인자로 사용자의 입력 데이터가 사용될 경우 악의적인 데이터를 입력해 원하는 프로그램 코드를 실행할 수 있기 때문에 사용자의 입력 데이터가 eval의 인자로 사용되지 않아야 한다.
-php
<?php
eval("1+1"); // 2
?>
-python
eval("1+1") # 2
exec("2+2") # 4
# eval은 하나의 식만을 처리하지만, exec은 하나의 문장을 처리합니다.
eval("a=1") # SyntaxError: invalid syntax
exec("a=1") # a=1
-javascript
eval("1+1"); // 2
<OS Command Function> OS Command를 실행하기 위한 어플리케이션 함수 사용시 실행하는 명령어에 사용자의 입력 데이터가 포함될 경우 Command Injection 취약점이 발생할 수 있음
취약점 |
|
Python |
-os.system, popen -subprocess.call, run, popen |
PHP |
-system, passthru -shell_exec, backtick operator(ex 'ls') -popen, proc_open -exec |
Javascript |
-child_process.exec, spawn |
<Filesystem function>
정의) 어플리케이션에서 파일 시스템에 접근할 수 있는 함수들의 인자가 사용자의 입력 데이터 또는 변조될 가능성이 있는 변수들을 사용할 경우 서버의 파일 시스템을 공격하거나, 다른 취약점으로 악용될 수 있음
취약점) 파일 시스템을 공격하여 발생할 수 있는 대표적인 서버 피해
(1) File Read= 어플리케이션 코드, 설정 파일 정보 등의 노출
-python
open(filename).read()
-javascript
var fs = require('fs');
fs.readFileSync(filename);
-php
<?php
file_get_contents($filename);
fopen($filename, "r");
readfile($filename);
show_source($filename);
highlight_file($filename);
...
// include 는 리드하는 파일에 php태그가 포함될 경우 해당 php 태그를 해석하고 실행하는 기능도 포함되어 있습니다.
include $filename;
?>
(2) File Write= webshell 생성을 통한 원격 코드 실행 공격, 기존 설정 파일을 덮는 공격을 통해 운영체제 또는 어플리케이션 설정 변경
-python
open(filename, "w").write(data)
-javascript
var fs = require('fs');
fs.writeFileSync(filename, data);
-php
<?php
file_put_contents($filename, $data);
fopen($filename, "w");
...
?>
(3) Etc
-파일 복사를 통해 File Write와 유사한 상황 발생
-설정 파일을 삭제하여 운영체제 또는 어플리케이션 서비스 무력화
<serialize / deserialize(직렬화/ 역직렬화)>
serialize(직렬화)= <object 또는 data의 상태 또는 타입> -> <특정한 형태의 포맷을 가진 데이터>
직렬화는 오브젝트, 데이터의 현재 상태와 타입들을 저장하고, 원하는 상황에 역직렬화를 통해 동일한 상태와 타입을 가진 오브젝트 또는 데이터들을 사용할 수 있음
deserialize(역직렬화)= <직렬화된 데이터> -> <원래의 object 또는 data의 상태 또는 타입>
역직렬화 과정에서 어플리케이션 상에서 다른 행위를 발생시키는 상태 또는 타입을 이용하여 악의적인 행위를 발생시키거나, 특정한 상황에서 호출되는 메소드들을 이용하여 공격에 사용
import pickle
class TestClass:
def __init__(self, a, b):
self.A = a
self.B = b
# TestClass 생성, ClassA로 할당
ClassA = TestClass(31337,10001)
# ClassA 직렬화
ClassA_dump = pickle.dumps(ClassA)
print(ClassA_dump)
# ClassA 역직렬화, ClassB로 할당
ClassB = pickle.loads(ClassA_dump)
print(ClassB.A, ClassB.B)
<언어별 직렬화/역직렬화 모듈>
-python: pickle, yaml
(python object의 __reduce__ 메소드를 이용하여 공격에 사용
<python deserialize 취약점 예시 코드>
import pickle
import os
class TestClass:
def __reduce__(self):
return os.system, ("id", )
ClassA = TestClass()
# ClassA 직렬화
ClassA_dump = pickle.dumps(ClassA)
print(ClassA_dump)
# 역직렬화
pickle.loads(ClassA_dump)
-javascript: node-serialize
serialize_func_1 = {"test":"_$$ND_FUNC$$_function(){ return 'Hello'; }"}
console.log(serialize.unserialize(serialize_func_1));
/*
{ test: [Function (anonymous)] }
*/
serialize.unserialize(serialize_func_1)['test']()
/*
'Hello'
*/
역직렬화 -> 원래 object 형태로 나옴
serialize_func_2 = {"test":"_$$ND_FUNC$$_function(){ return 'Hello'; }()"}
console.log(serialize.unserialize(serialize_func_2));
/*
{ test: 'Hello' }
*/
직렬화된 object의 결과에서 내부에 포함된 함수가 역직렬화 과정에서 실행되게 ()를 붙여 실행
(node-serialize 모듈의 직렬화된 포맷에서 _$$ND_FUNC$$_는 함수를 의미)
-php
<?php
class Test{
var $var1 = "value1";
var $var2 = 31337;
var $var3 = True;
}
$obj = new Test();
var_dump(serialize($obj));
/*
string(73) "O:4:"Test":3:{s:4:"var1";s:6:"value1";s:4:"var2";i:31337;s:4:"var3";b:1;}"
*/
<변조 공격 예시>
<?php
class Test{
var $var1 = "value1";
var $var2 = 31337;
var $var3 = True;
}
$obj = new Test();
var_dump($obj);
/*
object(Test)#1 (3) {
["var1"]=>
string(6) "value1"
["var2"]=>
int(31337)
["var3"]=>
bool(true)
}
*/
$serialize_Data = 'O:4:"Test":3:{s:4:"var1";s:4:"test";s:4:"var2";i:77777;s:4:"var3";b:0;}';
$obj = unserialize($serialize_Data);
var_dump($obj);
/*
object(Test)#2 (3) {
["var1"]=>
string(4) "test"
["var2"]=>
int(77777)
["var3"]=>
bool(false)
}
*/
?>
PHP deserialize 공격에서는 object의 특수한 상황에서 실행되어지는 Magic Methods를 이용한다.
<Magic Methods>
__destruct() --> 소멸자의 개념으로 오브젝트 소멸 시 호출됨
__wakeup() --> 역직렬화 시 호출됨
<PHP Specific Vulnerability: PHP의 특징으로 인해 발생할 수 있는 취약점>
1)PHP - Include
PHP의 include 함수는 인자로 전달된 파일을 읽은 후 해당 파일의 내용을 출력한다. 파일의 내용 중 PHP 코드로 해석되는 구문이 존재하면 해당 코드를 실행한다.
<?php
{php code}
?>
<?={php code}?> # short echo tag; php코드의 결과가 출력됩니다.
include 함수는 동적으로 다른 PHP 페이지를 로드해야할 때 주로 사용한다. 페이지의 배경이나 기본 레이아웃은 고정 시킨 채 내용의 변경이 필요한 경우 include를 통해 다른 php파일을 포함시킬 수 있다.include와 비슷한 역할을 하는 함수로는 include_once, require, require_once 등이 있다.
include 함수는 파일의 확장자 또는 파일의 타입과는 상관없이 파일의 내용에 php 태그가 포함될 경우 php 코드가 실행되기 때문에 주의해야 한다.
include할 파일을 사용자의 입력 데이터에 의해 조작될 수 있다면 /index.php?page=/etc/passwd와 같이 요청하여 서버 내의 파일 정보를 출력하거나, 업로드할 수 있는 다른 기능과 연꼐하여 서버에서 원하는 PHP 코드를 실행해 공격에 사용할 수 있음
서버 로컬 파일을 Include 하는 취약점을 Local File Inclusion(LFI), 외부 자원을 Include하는 취약점을 Remote File Inclusion(RFI)라 부른다.
2)PHP-Wrapper
PHP는 include(), fopen(), copy(), file_exists(), filesize()와 같은 파일 시스템 함수에서 URL style 프로토콜을 위한 wrapper들이 존재한다.
파일 시스템 관련 함수로 filename이 사용자의 입력이 될 경우 php에 존재하는 wrapper를 사용해 개발자의 의도와 다른 행위를 발생시킬 수 있다.
(1) file:// — Accessing local filesystem
ex) include "file:///etc/passwd";
(2) http:// — Accessing HTTP(s) URLs
*allow_url_include=On
ex) include "http://example.com";
*allow_url_include=Off
ex) include "http://example.com";
(3) ftp:// — Accessing FTP(s) URLs
(4) php:// — Accessing various I/O streams
ex)
*php://stdin, php://stdout and php://stderr
stdin, stdout, stderr로 연결합니다.
*php://fd/<fd number>
file descriptor에 연결합니다.
*php://memory and php://temp
메모리에 연결합니다.
*php://input
HTTP의 body 즉, POST 데이터를 입력받습니다.
*php://filter
I/O 스트림에 특정 필터를 설정합니다.
cf) php에서 include시 php태그가 존재하면 php실행되고 해당 코드는 출력되지 않습니다. 하지만 base64등의 인코딩을 통해 php tag를 인코딩 시켜서 코드가 실행되지 않고 출력되도록하여 소스 코드를 노출시키는 방법을 많이 사용합니다.
(5) zlib:// — Compression Streams
(6) data:// — Data (RFC 2397)
ex) file_get_contents('data://text/plain;base64,SSBsb3ZlIFBIUAo=');
(7) glob:// — Find pathnames matching pattern
(8) phar:// — PHP Archive
3)PHP-Extract= PHP의 extract 함수는 배열에서 변수를 가져온다. extract 함수 시 기존에 사용되고 있는 변수의 데이터를 덮을 수 있기 때문에 extract 함수에 사용자의 입력($_GET, $_POST, $_FILES)과 같은 신뢰할 수 없는 데이터가 사용되면 다른 변수를 변조하여 공격에 사용될 수 있다. 따라서 php extract 문서에서는 유저데이터 처럼 신뢰할 수 없는 어떠한 데이터를 사용하지 않는 것을 권고하고 있다.
cf) Warning Do not use extract() on untrusted data, like user input (e.g. $_GET, $_FILES).
<?php
$systemCMD = "ping 127.0.0.1";
...
extract($_GET);
...
system($systemCMD);
/*
/?systemCMD=id
uid=1000(dreamhack) gid=1000(dreamhack) groups=1000(dreamhack)
*/
4)PHP-Type Juggling= PHP에서 서로 다른 타입인 변수를 비교 또는 연산 시 자동으로 형변환이 발생하며 의도치 않은 결과가 발생할 수 있다.
<?php
$a = "a"; // String
$b = "2"; // String
echo $a * 2; // 0
echo $b * 2; // 4
/*
String과 integer 비교 또는 연산 시 앞 문자열이 숫자로 시작할 경우 해당 숫자를 사용하여 연산합니다.
아닌 경우에는 0으로 처리됩니다.
*/
$a = "a";
$b = "";
var_dump($a == True); // bool(true)
var_dump($b == True); // bool(false)
/*
String과 Boolean 타입 비교 또는 연산 시 문자열의 내용이 존재하면 True, 빈 문자열인 경우 False로 처리됩니다.
*/
5)PHP- Comparison= 비교연산자(==)를 사용할 때 비교하는 두 변수의 타입이 다르면 type-juggling에 의해 타입이 변환된 후 비교한다. 정확한 비교를 하기 위해서는 동일한 타입인지도 비교하는 비교연산자(===)를 사용해야 한다.
<PHP 비교연산자 입력에 따른 결과>
<?php
var_dump(0 == "a"); // 0 == 0 -> true
var_dump("1" == "01"); // 1 == 1 -> true
var_dump("10" == "1e1"); // 10 == 10 -> true
var_dump(100 == "1e2"); // 100 == 100 -> true
var_dump(True == 1); // Boolean == Interger -> true
var_dump(0 === "a"); // false
var_dump("1" === "01"); // false
var_dump("10" === "1e1"); // false
var_dump(100 === "1e2"); // false
var_dump(True === 1); // false
<php에서의 문자열 비교 방식:strcmp함수>
strcmp ( string $str1, string $str2 ) : int
인자를 비교했을 때 같으면 0, $str1이 작으면 음수 $str2가 작으면 양수가 반환되고 string이 아닌 array를 인자로 줄 경우 NULL이 반환된다. PHP에서는 NULL == 0 와 같은 식으로 비교하게 될 경우 단순 Equal 연산이기 때문에 Type Juggling이 발생해 0 == 0으로 비교하게 되어 문제가 발생할 수 있음
<?php
var_dump(strcmp("a", array())); // NULL
var_dump(strcmp("a", array()) == 0); // NULL == 0 -> true
var_dump(strcmp("a", array()) === 0); // NULL === 0 -> false
※strcmp로 문자열을 비교할 때 equal(==)이 아닌 identical(===)을 사용해야 한다.
6)PHP- Session
<?php
session_start(); // session 사용을 위해 필수적으로 사용되어야 함.
$_SESSION['uid'] = 1234; // 해당 유저의 session['uid']에 int 1234 로 할당
<php 세션 관리 로직>
1. session_start()
-HTTP Request에서 쿠키를 가져와 PHPSESSID(php 기본 Session ID, 쿠키를 통해 서버와 사용자가 주고받는 php session의 이름)의 값을 가져온다.
-PHPSESSID값이 존재하지 않으면 랜덤한 값의 PHPSESSID를 생성하고 세션 변수에($_SESSION) 빈 array를 할당한다. PHPSESSID값이 있으면 해당 PHPSESSID에 해당하는 값을 세션 변수에($_SESSION) 로드한다.
2. $_SESSION 변수에 접근하여 값을 읽거나 변경
3. session_start로 시작된 해당 페이지(PHP)가 종료될 때 자동으로 세션 변수를 serialize한 값을 파일 시스템에 저장
cf) 기본적으로 파일 시스템의 {세션기본경로}/sess_{PHPSESSID값} 경로에 저장된다.
★리눅스 시스템의 경우 기본 설정이라면 세션 기본 경로가 /var/lib/php/sessions 로 설정되는 시스템이 대부분이다. 또한 세션에서 사용되는 키(PHPSESSID)를 사용자는 쿠키의 정보를 확인하여 알 수 있기 때문에 서버의 파일 시스템에서 세션 파일이 저장되는 경로와 파일을 추측해볼 수 있다.
세션에 사용자의 입력 데이터가 들어가게 된다면 다른 공격과 연계하여 사용될 수 있다.
예를 들어 include함수 인자를 조작 가능하고 인자에 대한 검증이 없는 페이지에서 세션 파일을 include 인자로 넘겨 원하는 PHP 코드를 실행할 수 있다.
(1)
<?php
session_start();
$_SESSION['name'] = $_GET['name']; // 사용자 입력 데이터를 세션에 저장.
(2) 위와 같은 파일이 존재할 때 /session.php?name=<?php system('id');?>와 같이 입력하여
php 코드를 세션에 삽입
(3) 실행화면
# ls -al /var/lib/php/sessions/
total 28
drwx-wx-wt 2 root root 4096 Mar 6 04:32 .
drwxr-xr-x 4 root root 4096 Sep 2 2019 ..
-rw------- 1 www-data www-data 34 Jan 31 08:52 sess_d54ejehuaq8ksuoatpeph898q3
# cat sess_d54ejehuaq8ksuoatpeph898q3
name|s:21:"<?php system('id');?>";
7)PHP- upload logic
<php 파일 업로드 내부 로직>
(1) 임시 디렉토리에 임시 파일 생성(기본적으로 /tmp 사용)
(2) php 로직 중 업로드 파일 처리 로직 존재 시 임시파일 참조
($_FILES['file']['tmp_name'])
(3) php 로직 처리 후 임시 파일 삭제
(cf. php 로직= move_uploaded_file 함수에 의해 서버 코드가 설정된 경로로 파일을 옮김)
php 코드에서 로직 처리 등의 이유로 지연이 발생한다면 임시 파일은 지연이 발생하는 시간동안 존재한다. 시간 지연이 발생하는 페이지에 파일을 업로드한 후 해당 서버 임시 디렉토리를 확인하면 아래와 같이 임시 파일이 생성된 것을 확인할 수 있다.(임시 파일명 규칙: php[a-zA-Z0-9]{6})
(발생할 수 있는 문제점)
php 코드상에서 업로드 기능을 구현하지 않아도 사용자가 서버의 임시 디렉토리에 업로드 가능.
php의 기본 설정에서 사용자의 요청 중 파일이 존재한다면 코드상에서 처리하는 로직이 존재하지 않더라도 임시 파일을 생성한다. 원하는 파일 경로나 파일 명으로 서버 파일 시스템에 존재하는 것은 아니지만, 원하는 파일 데이터가 서버의 예측 가능한 디렉토리(임시 디렉토리)에 작성될 수 있다.
또한 php 엔진의 문제를 발생시키는 Unhandled exception(php 웹 어플리케이션 코드 실행 중 php엔진의 문제가 발생하는 경우, segmentation fault)이 발생하게 되면 php 로직이 정상적으로 종료되지 않으며, php 파일 업로드 내부 로직의 마지막 과정인 임시 파일 삭제가 실행되지 않는다. 삭제되지 않은 임시 파일은 특별한 행위가 발생하기 전까지 임시 디렉토리에 남아 있게 된다
.
예측 가능한 임시 파일 생성 규칙.
임시 파일의 파일 명 생성 규칙은 php[a-zA-Z0-9]{6}입니다. 일반적으로 해당 파일 명을 한번에 찾기는 어렵지만, Unhandled exception를 이용하여 임시 디렉토리 내에 임시 파일이 삭제되지 않도록하고, 같은 파일 데이터를 가진 파일을 무한히 업로드하면 무작위 대입공격을 통해 충분히 예측 가능한 범위까지 줄일 수 있다. 파일 업로드 내부 로직에서 발생할 수 있는 문제점이 include 또는 파일 시스템에서 발생하는 취약점과 연계되면 서버의 명령어를 실행시키는 등의 공격으로 연계될 수도 있다.
<Javascript Specific Vulnerability:자바스크립트만의 특징으로 인해 발생하는 취약점>
자바스크립트도 PHP와 마찬가지로 변수의 타입을 동적으로 정할 수 있는 dynamic 언어이다. 타입이 다른 두 변수의 값을 비교할 때 어떻게 되는지 알아보고 그 과정에서 발생할 수 있는 문제점을 알아본다.
1)Comparison Problem= 자바스크립트는 두 값을 비교할 때 두 값 모두 객체가 아닐 경우 valueOf, toString 함수를 이용해 원시값(primitive value)을 가져와 비교한다.
var a = {}
console.log(a.toString()); // [object Object]
a.toString = () => 'dreamhack';
console.log(a == 'dreamhack'); // true
a.valueOf = () => 'dreamhack2';
console.log(a == 'dreamhack2'); // true
2번 라인은 객체 a의 문자열 원시 값을 가져온다. 그 결과 [object Object]인 것을 확인할 수 있다.
4번 라인은 toString 함수를 dreamhack이라는 문자열을 반환하게 수정한 뒤 a 객체를 문자열과 비교한다.
그 결과 true를 반환하는 것을 확인할 수 있다. 이를 통해 문자열과 비교할 경우 toString을 통해 문자열 원시 값을 가져온다는 사실을 알 수 있다.
7번 라인에서는 toString 대신 valueOf 함수를 dreamhack2로 반환하는 함수로 수정한다. 이번에는 dreamhack2와 비교를 해보면 마찬가지로 true를 반환하는 것을 볼 수 있다.
->toString보다 valueOf 함수가 원시 값을 구하기 위해 먼저 호출된다는 것을 알 수 있다.
2)Array's string primitive
var req = {secret: A}
if ("secretValue" == req.secret) {
data = req.secret.charAt(0)
/* ... */
}
A 값을 수정해 3번 라인에서 익셉션이 발생하게 할 수 있다. 배열의 문자열 원시 값은 Array.join(array, ',')와 일치 한다. 따라서 첫번째 요소가 secretValue인 배열을 A에 넣어주면 charAt을 찾지 못해 익셉션이 발생한다.
-문자열 타입 입력시
var req = {secret: A}
if ("secretValue" == req.secret) {
data = req.secret.charAt(0)
}
// "s"
--배열 타입 입력시
var req = {secret: ["secretValue"]}
if ("secretValue" == req.secret) {
data = req.secret.charAt(0);
}
// Uncaught TypeError: req.secret.charAt is not a function
문자열 타입을 입력한 비교문에서는 정상적으로 검증, 그러나 배열 타입 입력시 charAt 함수는 Array에는 존재하지 않는 함수이기에 오류 발생
3)Prototype Pollution
var a = {0: 1}
var b = {1: 2}
console.log(a.__proto__ == b.__proto__) // true
console.log(b.__proto__ == Object.prototype) // true
자바스크립트는 프로토타입을 사용하는 언어로, 객체를 생성할 때 초기화 할 프로토타입을 지정해 주지 않으면 object.prototype의 속성과 정의된 함수(method)를 상속 받는다. 객체는 __proto__를 이용해 자신이 가르키고 있는 프로토타입에 접근할 수 있다. 또한 같은 프로토타입을 상속받은 모든 객체는 메모리 상 같은 주소를 가리키고 있다. 위 코드와 같이 초기화 할 프로토타입을 지정해주지 않은 a와 b 객체의 __proto__는 동일한 것을 확인할 수 있으며, object.prototype과 일치한다.
4)Prototype chain lookup - 프로토 타입은 언제 사용되는가?
var x = {a: 0}
x.__proto__.a = 1337
x.__proto__.b = 1
Object.prototype.c = 2
var y = Object.create(x); // x를 프로토타입으로 가지는 오브젝트 생성.
console.log(y.__proto__ == x); // true
var z = {}
console.log(z.__proto__ == y.__proto__); // false
console.log(z.__proto__ == x.__proto__); // true
console.log(z.__proto__ == y.__proto__.__proto__); // true
console.log(y.a); // 0
console.log(y.b); // 1
console.log(y.c); // 2
console.log(x.a); // 0
console.log(x.b); // 1
console.log(x.c); // 2
console.log(z.a); // 1337
console.log(z.b); // 1
console.log(z.c); // 2
첫번째 라인에서 object.prototype을 상속받는 기본 객체를 할당한다.
x.__proto__에 a,b를 각각 할당한 후 object.prototype에 c를 할당한다.
그 후 x를 프로토타입으로 하는 객체 y를 생성한다.
y의 프로토타입은 object.prototype과 다르다는 것을 알 수 있다.
y.c를 출력했을 때 undefined가 아닌 2가 나오는 것을 통해 프로토타입에 원하는 속성이 없으면 계속해서 상위 프로토타입에서 찾는다는 것을 볼 수 있다.
즉, y 에 c가 없다면, y.__proto__ 에 c가 없다면, y.__proto__.__proto__의 c를 검색한다.
5)bracket notation
객체에 접근할 때 사용하는 방법은 .을 이용하거나 [xyz]를 사용하는 방법이 있다. 전자는 dot notation, 후자는 bracket notation이라고 부른다.
Bracket notation은 속성의 키를 문자열로 사용하기 때문에 변수를 넣거나 dot notation에서 사용할 수 없는 문자 (=, ', ")를 속성 키로 지정할 수 있다.
var a = {a: 0};
a["="] = "Equal Sign";
a["="]; // Equal Sign
a.=; // Uncaught SyntaxError: Unexpected token '='
6.Misconfiguration(잘못된 설정)
정의) 잘못된 설정(misconfiguration) 취약점은 웹 서버(apache,nginx), 데이터베이스 서버(mysql, postresql, mongodb), 캐시서버(redis), 웹 프레임워크(django,spring), 컨테이너(Docker, KBS)등 모든 웹 어플리케이션 계층에서 발생할 수 있음
발생원인)
(다른 웹 취약점처럼 소스코드 상에 존재하는 복잡한 로직에 의해 발생하는 취약점이 아니라)
간단한 설정 오류로 인해 발생
-> 기본 메뉴얼에 존재하는 룰을 잘 따랐는지만 확인하면 자동화된 도구로 발견하기 쉽다
<misconfiguration이 발생하는 원인>
(1)부주의로 인해 발생하는 문제점= 메뉴얼에 존재하지 않은 설정이거나 개발자의 부주의로 인해 설정 유무를 알지 못한 상태에서 서비스를 하는 경우 자주 발생
-권한 설정 문제
[1]잘못된 권한 설정
(1) 웹 서버를 운용하는 시스템 또는 어플리케이션 상에서 필요 이상의 권한을 부여하는 경우 문제 발생 가능
(2) 서비스를 운용하는 OS의 사용자 권한이 일반 유저 또는 시스템 운용을 위해 생성된 계정을 사용하지 않고 superuser(root) 권한으로 사용할 경우 서비스의 로직이 superuser권한으로 수행된다.
ex) 웹 서버가 superuser권한으로 동작하며, 파일 관련 취약점이 존재할 경우 일반 사용자 권한이 접근하지 못하는 시스템 파일등에도 접근이 가능하게 되며, 이를 통해 시스템 장악의 취약점으로 연계
(3) DB, Docker 등의 어플리케이션 운용 시에도 해당 어플리케이션의 사용자 권한에 필요 이상의 권한을 부여할 경우 문제 발생 가능
ex) DBMS 내에서 root 계정을 사용하게 되면 기본적으로 몯느 데이터베이스에 접근이 가능하며, 일반 유저가 사용하지 못하는 함수들도 사용할 수 있음. 만약 sql injeciton 취약점이 발생하게 되면 root권한이 사용할 수 있는 함수들을 통해 다른 취약점으로 연계될 수 있는 위협 존재
[2]기본 계정= 프레임워크 등에서 기본적으로 제공하는 계정을 삭제하지 않아 문제 발생.
프레임워크 등을 통해 제공되는 계정 정보는 ID/PW등의 정보가 메뉴얼 등을 통해 공개되어 있기 때문에 이를 통해 특별한 취약점을 사용하지 않고 권한을 획득할 수 있음
-기본 서비스
서비스 요소들 운용 시 사용하는 시스템 또는 어플리케이션에서 개발자가 설정하지 않아도 기본적으로 제공되는 기능에 의해 문제가 발생할 수 있음. 대부분 개발 당시에 필요한 기능이거나, 내부적으로 사용되어야 하는 기능들로 운영 시에 의도하지 않은 경로 또는 기능이 노출되는 위협이 발생할 수 있음.
->관리 서비스: Tomcat- Web Application Manager, Docker Registry API
->모니터링 서비스: Spring Boot Actuator-metrics, Apache mod_status
-임시/백업 파일, 개발 관련 파일
[3]임시/백업 파일= 웹 서버 디렉토리 내에 임시파일이나 백업파일이 존재한다면 디렉토리 스캐너를 통해 파일이 유출될 수 있음. 자주 사용되는 임시 파일과 백업파일은 swp,~,bak,sql등이 있음
ex) hello.php파일을 vim 에디터로 열게 되면 .hello.php.swp라는 임시파일이 생성된다.
웹서버는 .php 확장자를 php script로 인식해 실행 결과를 응답
그러나 .swp 확장자는 바이너리 파일로 인식해 파일 내용(소스 코드)가 포함된 임시파일을 응답
hello.php는 password가 일치하면 임의 명령을 실행해주는 php 파일이다. 공격자는 소스코드에 정의되어 있는 this_is_secret_password를 알 수 없다. 하지만 개발자가 vim 에디터로 수정 중에 생긴 .swp를 통해 소스코드를 획득할 수 있다.
.swp 파일을 다운로드
-> vim hello.php 명령어를 입력하여 메시지 확인
-> R키를 눌러 복구 진행
-> 복구가 되었다는 메시지와 함꼐 원본 소스코드 획득
<위험이 될 만한 확장자>
확장자 | 설명 |
bak | 백업 파일, 대부분의 에디터에서 사용함 |
config | 설정 파일, 비밀 키들이 존재하는 경우가 많음 |
sql | slq schema 파일, 데이터 베이스 구조를 알아낼 수 있음 |
sh | shell script 파일 |
- | bluefish 에디터 백업 파일 |
조치방안) 서비스하기 이전에 서버 및 어플리케이션의 설정 및 파일들을 점검하여 서비스와는 무관한 설정 및 파일들을 제거. 또한 위험한 확장자에 대한 요청이 들어올 경우 거부하는 방법으로 위협을 없앨 수 있음
location ~ (?:\.(?:bak|config|sql|fla|psd|ini|log|sh|inc|swp|dist)|~)$ {
deny all;
access_log off;
log_not_found off;
}
//ngix에서의 예시 설정
[4]개발 관련 파일= 버전 관리 도구(VCS)를 사용해 소스코드를 다운로드 받거나 웹 폴더내에서 직접 개발을 하다 보면 해당 VCS가 자동으로 생성하는 정보 파일이 남음
git은 .git 폴더가 생성되며 해당 폴더 내 트래킹 되고 있는 파일들의 소스코드를 쉽게 획득할 수 있음. init 과정 보다 git clone..을 통해 저장소를 받아온 후 웹 서버가 바로 서빙되는 경우 발생하기 쉬움
조치방안) 임시/백업파일과 마찬가지로 서비스하기 이전에 점검하여 서비스와는 무관한 파일들을 제거. 또한, 웹 URL을 통해 접근하는 것을 차단. 웹 서버의 설정으로 쉽게 막을 수 있음
location ~ /\.(git|hg) {
deny all;
}
//ngix 에서의 예시 설정
(2)편의성을 위한 설정에 의해 발생하는 문제점= 개발자를 위한 설정이 서비스 환경에서도 켜져있어 공격자가 이를 통해 시스템 내부의 정보를 알아내 추가적인 공격에 사용될 수 있음
-DEBUG / Error Message Disclosure
[1] DEBUG= 디버그 모드 설정 또는 디버그 목적으로 코드 상에서 특정 정보를 사용자에게 제공할 경우 해당 정보를 기반으로 서버의 환경 및 정보 노출 가능
서버 환경설정 또는 프레임워크 등의 어플리케이션 구동 시 환경 설정의 debug 옵션을 설정한 후 운용할 경우, 사용자의 입력 데이터에 의해 에러가 발생하게 되면 해당 에러에 대한 정보가 노출될 수 있음. 악의적인 공격자는 이런 에러 정보를 통해 서버를 공격하기 위한 기반 정보들을 획득
debug 모드가 설정된 상태에서 에러가 발생하게 되면 위 그림과 같이 에러의 정보와 에러가 발생하는 서버 코드가 함께 노출되는 위협이 발생하기도 한다.
[2] Error Message Disclosure
코드 상에서 사용하는 변수 또는 정보가 디버그 목적 등으로 사용자들에게 노출되는 경우도 있음
#!/usr/bin/env python3
from flask import Flask, request
app = Flask(__name__)
@app.route('/')
def index():
return 'hi'
@app.route('/download')
def download():
try:
file = request.args.get("file")
return open(file).read()
except Exception as e:
return str(e.args)
app.run(host='0.0.0.0', port=8000)
-0.0.0.0으로 바인딩 된 네트워크 설정
내부 서비스 네트워크 또는 특정 네트워크에서만 접근할 수 있도록 접근 제어가 이루어져야 할 서버들이 개발 또는 운용상의 편의를 위해 0.0.0.0으로 바인딩 되어있다가 서비스 환경으로 변경되었음에도 바인딩 주소를 그대로 사용할 때 발생-> 이를 통해 악의적인 공격자는 인증이 없거나 취약한 내부 서비스에 접근할 수 있게 됨
<서비스 목록>
이름 | 용도 |
redis | cache 서버 |
mysql | RDBMS 서버 |
k8s master node | 클러스터 관리 |
ex) 공격자는 기본적으로 인증을 사용하지 않거나 취약한 기본 계정을 자주 사용하는 서비스를 대상으로 공격한다. bind 주소를 0.0.0.0으로 설정해 모든 IP Address로 부터 오는 요청을 허용. 그 결과 공격자는 외부 인터넷 환경에서 해당 redis 서버에 접근할 수 있고 redis 기본 설정 상 인증 절차가 없기 때문에 접근만으로 임의 redis 명령을 수행할 수 있음.
조치방안)
<1> 해당 방법을 이용해 허용할 포트를 제외한 설정은 모두 삭제
(인스턴스 이미지를 복사해 방화벽이 허용된 곳에서 사용한다면 취약하다.)
<2> 설정 파일에서 취약한 부분을 찾아 패치
:ubuntu 환경에서 apt로 redis 패키지를 설치하였다면 /etc/redis/redis.conf 경로에 설정파일이 있음
0.0.0.0으로 바인딩 되어있는 라인을 찾아 삭제하고 bind 127.0.0.1 ::1 로 설정해 로컬 머신에서만 접근할 수 있도록 해야 한다. 내부 서비스 네트워크에서만 접속하게 하려면 접속하는 대상의 IP를 명시해 추가해야한다.
(3)메뉴얼과 실제 구현체의 차이로 인해 발생하는 문제점= 메뉴얼에 설명되어 있는 대로 사용했지만 중의적 표현이 존재하거나 잘못 설명되어있을 때 발생
-Nginx Alias Path Traversal 취약점
Nginx에서 경로를 설정 할 때 alias, root 두 가지 방식이 있음
alias: 요청한 경로를 지시한 다른 경로로 변경
root: 해당 경로의 root 경로를 명시
http://server/i/top.gif를 요청하면
alias) /data/w3/images/top.gif
root) /data/w3/images/images/top.gif를 가져온다.
한 단계 상위 디렉토리의 파일을 가져올 수 있게 된다.
<취약한 설정 확인과 strace를 이용한 nginx worker의 동작 확인>
[00007f64a29e57ea] recvfrom(3, "GET /dreamhack/e.xxd HTTP/1.1\r\nA"..., 1024, 0, NULL, NULL) = 319
[00007f64a29e5d2b] openat(AT_FDCWD, "/tmp//e.xxd", O_RDONLY|O_NONBLOCK) = 12
...
[00007f64a29e57ea] recvfrom(3, "GET /dreamhack../etc/passwd HTTP"..., 1024, 0, NULL, NULL) = 326
[00007f64a29e5d2b] openat(AT_FDCWD, "/tmp/../etc/passwd", O_RDONLY|O_NONBLOCK) = 12
=> /dreamhack/e.xxd 를 요청했을 때 /tmp//e.xxd 를 읽어오는 것을 확인할 수 있고
/dreamhack../etc/passwd를 요청하면 /tmp/../etc/passwd를 읽어온다.
조치방안)
<1> alias 대신에 root directive를 사용
mkdir /tmp/dreamhack
-----
location /dreamhack/ {
root /tmp/
}
<2> location 마지막에 /를 붙이거나 alias 맨 뒤 /를 삭제해 패치
[00007f64a29e57ea] recvfrom(3, "GET /dreamhack/e.xxd HTTP/1.1\r\nA"..., 1024, 0, NULL, NULL) = 319
[00007f64a29e5d2b] openat(AT_FDCWD, "/tmp/e.xxd", O_RDONLY|O_NONBLOCK) = 12
[00007f64a29e57ea] recvfrom(3, "GET /dreamhack../etc/passwd HTTP"..., 1024, 0, NULL, NULL) = 326
[00007f64a29e5d2b] openat(AT_FDCWD, "/tmp../etc/passwd", O_RDONLY|O_NONBLOCK) = -1 ENOENT (No such file or directory)
정상적인 요청은 openat이 성공하고 공격자의 요청은 ENOENT 에러와 함께 실패한다.
[2]NGINX Proxy SSRF
Open-Proxy가 될 수 있다고 답변 작성자는 경고했지만 제대로 읽지 않고 그대로 사용할 경우 /image/ location에서 server-side-request-forgery(SSRF) 취약점이 발생해 nginx를 통해 내부 서비스 네트워크에 접근할 수 있게 된다.
조치방안) SSRF 취약점으로 부터 안전해지기 위해서 설정 파일을 수정해야한다. 개발자가 미리 정의한 호스트에게만 요청할 수 있도록 변경
location ~ ^/image/(https?):/((?:hostA|hostB)(?::\d+)?)/(.*) {
proxy_pass $1://$2/$3;
}
hostA와 hostB를 제외한 어떤 호스트도 프록시 패스를 허용하지 않는 설정
(4)해당 코드나 설정에 대한 이해 없이 사용해 발생하는 문제점= 취약점이 존재하는 예제코드, StackOverflow 답변등을 이해 없이 복사 붙여넣기 해 사용하거나 메뉴얼에 존재하는 권고 설정을 무시한 채 사용해 발생
-stackoverflow
-모든 도메인을 허용한 CORS 설정
※ 해당 글의 모든 사진과 내용의 출처는 Dreamhack.io 에 있습니다 ※
'1. Web hacking (웹 해킹) > 2) 개념 정리' 카테고리의 다른 글
[2020.9.19] DreamHack 개념 정리 - Client-side Basic (0) | 2020.09.24 |
---|---|
[2020.05.19] Dreamhack 개념정리 - 5강 Server-side Advanced - SQLInjection (0) | 2020.05.21 |
[2020.05.19] Dreamhack 개념정리 - 4강 Client-side Advanced (0) | 2020.05.20 |
[20.03.31] Dreamhack 개념정리 - Introduction of Web hacking (0) | 2020.04.26 |
[2020.03.31] Dreamhack 개념정리 - Client-side Basic (0) | 2020.04.25 |