웹 5차시

이동규·2026년 9월 1일

Layer7과제

목록 보기
22/22

Rednose airline

문제 설명

안녕하세요 저는 객실승무원 B0ramae, 보라매 입니다.
저희 Rednose Airlines 는 승객여러분을 환영합니다!
빨간코 항공을 이용해주시는 여러.. 아 승객이 아니세요??
아 죄송해요.. 이쪽으로 모시겠습니다
근데 누구세요?

API 서버로의 공격을 절대 금지합니다.

문제풀이

# 베이스 이미지 설정
FROM python:3.8

# 작업 디렉토리 설정
WORKDIR /deploy

# Python 스크립트 및 텍스트 파일 복사
COPY ./deploy /deploy/

# Flask 설치
RUN pip install flask pyjwt

RUN FLAG=$(cat /dev/urandom | tr -dc 'a-z' | fold -w 4 | head -n 1); mv /deploy/flag.txt /deploy/flag_$FLAG.txt

EXPOSE 13000
# main.py 실행
CMD ["python", "/deploy/main.py"]

도커파일을 살펴보자
플래그는 /deploy/flag_(a-z랜덤글자4연속).txt에 위치한 것을 확인할 수 있다

그렇다면 파일을 볼 수 있는 곳이 있는지 확인해 보자

@app.route('/api/metar')
def metar():
    global request_count
    global timeout_until
    current_time = datetime.now()
    if timeout_until and current_time < timeout_until:
        remaining_time = timeout_until - current_time
        return "Timeout! {}".format(remaining_time), 429
    if request.cookies.get('auth'):
        try:
            decode_jwt = jwt.decode(request.cookies.get('auth'), JWTKey, algorithms=['HS256'])
            if decode_jwt['id'] == 'admin' and decode_jwt['isAdmin'] == True:
                request_count += 1
                if request_count >= RATE_LIMIT_REQUESTS:
                    timeout_until = current_time + RATE_LIMIT_TIMEOUT
                    request_count = 0 
                airport = request.args.get('airport')
                result = subprocess.run(['curl', airport], shell = False, stdout=subprocess.PIPE, stderr=subprocess.DEVNULL, text=True)                    
                return result.stdout
            else:
                return render_template_string('You are not admin.')
        except Exception as e:
            print(e)
            return send_file("./cat/fail.jpg", mimetype='image/jpg') # BAD JWT
    else:
        return render_template_string('<script>alert("You are not logged in.");history.back()</script>')

/api/metar 경로에는 airport라는 파라미터에서 argument를 읽어와 curl로 서버에 요청을 보내고 응답을 확인한다

그러므로 우리는 이경로에

/api/metar?airport=file:///deploy/flag_[a-z][a-z][a-z][a-z].txt

이런 식으로 접근하면 플래그를 획득할 수 있다

하지만 여기서 문제 한가지에 직면한다

jwt로 cookie에 저장된 auth 값을 읽어와 id 가 admin이고 isAdmin이 true인지 확인하므로 admin계정을 얻을 필요가 있다

@app.route('/login', methods=['GET', 'POST'])
def login():
    if request.method == 'GET':
        if request.cookies.get('auth'):
            return render_template_string('<script>alert("You are already logged in.");history.back()</script>')
        return render_template('login.html')
    elif request.method == 'POST':
        form_id = request.form['id']
        form_pw = request.form['pw']
        print(form_id, form_pw)
        if len(form_id) > 10:
            return "Too Long"
        elif len(form_id) < 3:
            return "Too Short"
        elif not re.search(r"^[A-Za-z{}]*$", form_id):
            return "Blacklist Word"

        user = next((user for user in users if user['id'] == form_id), None)
        if user and user['pw'] == form_pw:
            payload = {
                'id': form_id,
                'isAdmin': (form_id == 'admin'),
            }
            encode_jwt = jwt.encode(payload, JWTKey, algorithm='HS256')
            resp = make_response(render_template_string('Login Success!'))
            resp.set_cookie('auth', encode_jwt)
            return resp
        else:
            return render_template_string('{form_id} is not registered, or the password is incorrect.'.format(form_id=form_id))
        

/login 경로를 살펴보자

우선
{form_id}에 사용자의 id 입력이 들어가는 것을 확인할 수 있는데 여기서 ssti 취약점을 이용할 수 있다.

id = {{config}}
pw = 아무거나

를 입력한다

config에 있는 내용들을 볼 수 있게 되는데

우리는 jwt를 auth라는 이름을 가진 cookie에서 value를 읽어와 검증한다는 것을 이용하면 admin인 척 속일 수 있다

현재 secret
eeWStwDUfbtNAqirzyOcaCJAPFfXenMcdIhHpvYmGKWBDpFipK

payload = {
                'id': form_id,
                'isAdmin': (form_id == 'admin'),
            }
            encode_jwt = jwt.encode(payload, JWTKey, algorithm='HS256')

헤더와 payload 내용은 이러니 알맞게 수정해주면

{
  "alg": "HS256",
  "typ": "JWT"
}

{
	"id": "admin",
    "isAdmin": true
}

eeWStwDUfbtNAqirzyOcaCJAPFfXenMcdIhHpvYmGKWBDpFipK


이제 아까 /api/metar경로에 문제 없이 접근할 수 있다

그러므로

/api/metar?airport=file:///deploy/flag_[a-z][a-z][a-z][a-z].txt


시간이 조금 걸리겠지만 플래그를 획득할 수 있다

SSTI(Server Side Template Injection)

공격자가 서버 측에서 사용하는 템플릿 문법을 입력값에 삽입하여, 서버가 이를 단순한 문자열이 아닌 템플릿 코드로 해석하고 실행하면서 발생하는 취약점이다.

SSTI가 발생하면 템플릿 내부의 변수나 객체에 접근할 수 있으며, 경우에 따라 민감한 정보 노출이나 원격 코드 실행(RCE)으로 이어질 수 있다.

웹 템플릿 엔진

웹 템플릿 엔진은 웹 페이지의 기본 구조와 동적인 콘텐츠를 결합하여 HTML 문서를 생성하는 소프트웨어를 의미한다.

지금까지 공부한 내용 중에서는 Flask에서 사용하는 Jinja2 템플릿 엔진을 예시로 들 수 있다.

예를 들어 Jinja2에서는 다음과 같은 문법으로 변수의 값을 출력할 수 있다.

{{ username }}

서버는 {{ username }}을 그대로 출력하는 것이 아니라, username 변수에 저장된 값을 가져와 HTML에 삽입한다.

따라서 사용자의 입력이 템플릿 문법으로 처리되면 SSTI가 발생할 수 있다.

config

Flask 애플리케이션에서는 설정 정보가 config 객체에 저장되는 경우가 많다.

설정 정보에는 데이터베이스 주소, 비밀키, 환경 변수 등 중요한 정보가 포함될 수 있다.

따라서 템플릿에 다음과 같은 입력이 전달되고 실행된다면,

{{ config }}

Flask의 설정 정보가 화면에 출력되어 민감한 정보를 획득할 수도 있다.

단, 실제로 어떤 객체에 접근할 수 있는지는 템플릿이 실행되는 환경과 애플리케이션의 설정에 따라 달라진다.

RCE

RCE(Remote Code Execution)는 공격자가 원격 시스템에 임의의 명령어나 코드를 실행할 수 있는 취약점이다.

SSTI로 템플릿 내부의 객체나 함수에 접근할 수 있는 경우, 서버가 제공하는 기능을 악용하여 RCE로 이어질 수 있다.

RCE가 발생하면 서버의 파일을 읽거나 시스템 명령을 실행하는 등 큰 피해가 발생할 수 있으므로, 사용자 입력을 템플릿 코드로 직접 처리하지 않는 것이 중요하다.

JWT(JSON Web Token)

JWT란 사용자 인증과 정보 전달에 사용되는 JSON 기반의 토큰이다

JWT는 내용을 암호화하는 것이 아니라 Base64 URL-safe 방식으로 인코딩하고 전자 서명을 추가하여 토큰이 변조되지 않았는지 확인한다

따라서 Payload에는 비밀번호와 같은 민감한 정보를 저장하면 안 된다

구조

JWT는 일반적으로 다음과 같이 세 부분을 점(.)으로 이어 붙인 구조이다

Header.Payload.Signature

Header에는 토큰의 종류와 서명에 사용할 알고리즘이 담겨 있다

{
  "alg": "HS256",
  "typ": "JWT"
}
  • alg: 사용할 서명 알고리즘
  • typ: 토큰의 타입

payload

Payload에는 사용자 식별자, 권한, 만료 시간과 같은 정보가 담긴다

{
  "id": "admin",
  "isAdmin": true
}

Payload는 누구나 디코딩할 수 있으므로 암호화된 정보가 아니다

사용자가 Payload를 임의로 수정할 수는 있지만 Signature가 일치하지 않기 때문에 서버의 검증을 통과할 수 없다

signature

Signature는 Header와 Payload를 각각 Base64 URL-safe 방식으로 인코딩하고 두 값을 점으로 이어 붙인 데이터를 서명하여 생성한다

signing_input = Base64URL(Header) + "." + Base64URL(Payload)

이후 Header의 alg에 지정된 알고리즘과 키를 사용하여 서명을 만든다

HS256은 서버의 비밀키를 사용하여 HMAC-SHA256 방식으로 서명한다

HMAC-SHA256(signing_input, secret_key)

반면 RS256과 같은 방식은 개인키로 서명을 생성하고 공개키로 검증한다

생성된 서명값도 Base64 URL-safe 방식으로 인코딩하고 다음과 같이 세 부분을 이어 붙이면 최종 JWT가 완성된다

Base64URL(Header).Base64URL(Payload).Base64URL(Signature)

예시 :

eeWStwDUfbtNAqirzyOcaCJAPFfXenMcdIhHpvYmGKWBDpFipK

서버는 JWT를 받은 후 같은 키를 사용하여 Signature를 다시 계산하고 기존 Signature와 비교한다
두 값이 일치하면 Header와 Payload가 변조되지 않았다고 판단한다

HTTP는 기본적으로 각각의 요청을 서로 다른 요청으로 처리하며 이전 요청의 사용자를 기억하지 못한다

이를 보완하기 위해 cookie와 session을 사용한다

브라우저에 저장되는 데이터
key=value 형태로 저장
이후 요청에 자동으로 서버에 전송
로그인 상태, 사용자 설정 등을 저장할 수 있다

Session

사용자의 정보를 서버에 저장하는 방식
로그인 여부, 사용자 ID 등의 정보가 서버에 저장
브라우저에는 서버의 세션을 찾기 위한 session ID만 저장

Session ID

Session ID는 서버에 저장된 특정 session을 식별하기 위한 고유한 값이다

브라우저 cookie
session_id=abc123

서버 session
abc123 → user_id=7, logged_in=true

사용자가 로그인하면 서버는 session을 만들고 abc123과 같은 session ID를 발급한다

브라우저는 이를 cookie에 저장한다

이후 요청마다 cookie를 서버에 보내면 서버는 session ID를 이용해 해당 사용자의 정보를 찾아 로그인 상태를 확인한다

로그인
서버가 session 생성
session ID를 cookie에 저장
브라우저가 요청마다 session ID 전송
서버가 session ID로 사용자 확인

cookie는 session ID를 저장하는 장소가 될 수 있고 session ID는 서버의 session을 찾기 위한 식별자이다.

0개의 댓글