[WEB] SuperSecure OTP 문제풀이

문제를 확인해보자.

 

OTP가 구현된 서비스에서 FLAG를 획득하는 문제이다. 접속해보자.

 

 

admin/admin, guest/guest 로 접속이 당연히 안된다. 별걸 기대하자.

guest/guest를 입력하고 회원가입 버튼을 클릭하자.

 

 

로그인하면 요런 페이지가 나온다.

가입자 목록 보기 페이지에 접근해보자.

 

 

가입자 목록을 보려면 OTP 인증 값이 필요하다고 한다.

OTP 관리 페이지에 접속해보자.

 

 

필자는 한탄스럽게도 아이폰 유저이므로 앱스토어에서 Google Authenticator 앱을 설치해서

QR 코드 스캔을 통해 OTP를 생성해보자.

 

QR 코드를 스캔하면 위와 같이 guest 계정에 OTP 인증코드가 생성된다.

OTP 코드로 실제로 인증이 수행되는지 확인해보자.

 

 

아주 깔끔하게 동작한다. OTP 생성코드는 주기적으로 바뀌므로

이후 진행되는 과정에서 OTP 코드는 임의 숫자로 변경될 수 있음을 참고하자.

가입자 목록을 다시 확인해보자.

 

 

응답 값에서 해시화?된 패스워드 값이 보인다.

crackstation.net에서 해당 값을 확인해보자.

id userpw
admin febd93f04bda1aec0d374f8fd014d062525934feb1f1b81ee7c64d61f66b84b1
guest 84983c60f7daadc1cb8698621f802c0d9f9a3c3c295c810748fb048115c186ec

 

 

admin 계정은 admin/1q2w3e4r! 로 관리자는 군필 출신인 것으로 확인된다.

일단 플래그를 획득해보자.

 

 

관리자 계정으로만 실행이 가능하다고 한다.

가입자 목록에서 확인한 admin 계정으로 로그인해서 플래그를 획득하는 문제인 것으로 보인다.

 

admin 계정으로 로그인해서 OTP 코드를 새로 만들고 플래그를 획득해보자.

 

 

아니 admin으로 OTP 생성을 하려고하니 안된다고 한다....

일단 추가정보가 없으므로 소스코드를 확인해보자.

 

from os import urandom
from hashlib import sha256
from flask import Flask, request, jsonify, send_from_directory
from flask_cors import CORS
from flask_jwt_extended import (
    JWTManager,
    jwt_required,
    create_access_token,
    get_jwt_identity,
)
import pyotp
from models import Users, OTPStorages, init_db

app = Flask(__name__, static_url_path="")
app.config["SQLALCHEMY_DATABASE_URI"] = "sqlite:///supersecureotp.db"
app.config["SQLALCHEMY_TRACK_MODIFICATIONS"] = False
app.secret_key = urandom(32)  # random 32 bytes key
db = init_db(app)
jwt = JWTManager(app)
CORS(app, resources={r"/*": {"origins": "*"}})
OTP_VALID_SEC = 120  # 2 minutes


@app.route("/", defaults={"path": ""})
@app.route("/<string:path>")
@app.route("/<path:path>")
def index(path):
    return send_from_directory(app.static_folder, "index.html")


@app.route("/signup", methods=["POST"])
def signup():
    if request.method == "POST":
        userid = request.json["userid"]
        userpw = request.json["userpw"]
        if userid and userpw:
            user = Users.query.filter_by(userid=userid).first()
            if len(userid) < 4 or len(userpw) < 4:
                return jsonify({"result": False, "msg": "ID 또는 PW가 4자 이상인지 확인해주세요."})
            if user:
                return jsonify({"result": False, "msg": "이미 가입된 이용자 입니다."})
            m = sha256()
            m.update(userpw.encode("latin-1"))
            generated_password_hash = m.hexdigest()
            new_user = Users(
                userid=userid, userpw=generated_password_hash, is_admin=False
            )
            user_otp = OTPStorages(
                userid=userid, otp_secret=pyotp.random_base32(), otp_verify=False
            )
            db.session.add(new_user)
            db.session.add(user_otp)
            db.session.commit()
            access_token = create_access_token(
                identity=new_user.userid, expires_delta=None
            )
            return jsonify({"result": True, "access_token": access_token})
        return jsonify({"result": False, "msg": "ID 또는 PW를 확인해주세요."})


@app.route("/signin", methods=["POST"])
def signin():
    if request.method == "POST":
        userid = request.json["userid"]
        userpw = request.json["userpw"]
        if userid and userpw:
            user = Users.query.filter_by(userid=userid).first()
            m = sha256()
            m.update(userpw.encode("latin-1"))
            generated_password_hash = m.hexdigest()
            if user and user.userpw == generated_password_hash:
                access_token = create_access_token(
                    identity=user.userid, expires_delta=None
                )
                return jsonify({"result": True, "access_token": access_token})
            else:
                return jsonify({"result": False, "msg": "ID/PW를 확인해주세요."})
    return jsonify({"result": False, "msg": "ID/PW를 확인해주세요."})


@app.route("/my_info", methods=["POST"])
@jwt_required()
def my_info():
    userid = get_jwt_identity()
    return jsonify({"result": True, "userid": userid})


@app.route("/otp_register", methods=["POST"])
@jwt_required()
def OTP_register():
    userid = get_jwt_identity()
    user = Users.query.filter_by(userid=userid).first()
    if user.is_admin:
        return jsonify({"result": False, "msg": "보안 정책상 관리자는 OTP 재등록이 불가능 합니다."})
    otp_storage = OTPStorages.query.filter_by(userid=userid).first()
    otp_url = pyotp.totp.TOTP(
        otp_storage.otp_secret, interval=OTP_VALID_SEC
    ).provisioning_uri(name=userid, issuer_name="SuperSecure OTP")

    return jsonify({"result": True, "otp_url": otp_url})


@app.route("/otp_view", methods=["POST"])
@jwt_required()
def OTP_view():
    if request.method == "POST":
        userid = get_jwt_identity()
        otp_storage = OTPStorages.query.filter_by(userid=userid).first()
        if otp_storage.otp_verify == False:
            return jsonify({"result": False, "msg": "OTP가 인증되지 않았습니다."})
        otp_storage.otp_verify = False
        db.session.commit()
        current_otp = pyotp.totp.TOTP(
            otp_storage.otp_secret, interval=OTP_VALID_SEC
        ).now()
        return jsonify({"result": True, "otp_code": current_otp})
    return jsonify({"result": False, "msg": "HTTP Method Error"})


@app.route("/otp_auth", methods=["POST"])
@jwt_required()
def OTP_auth():
    if request.method == "POST":
        otp_code = request.json["otp_code"]
        userid = get_jwt_identity()
        otp_storage = OTPStorages.query.filter_by(userid=userid).first()
        otp_result = pyotp.totp.TOTP(
            otp_storage.otp_secret, interval=OTP_VALID_SEC
        ).verify(otp_code, valid_window=OTP_VALID_SEC)
        if otp_result:
            otp_storage.otp_verify = True
            db.session.commit()
            return jsonify({"result": True, "msg": "Success"})
        else:
            return jsonify({"result": False, "msg": "Failed"})
    return jsonify({"result": False, "msg": "Failed"})


@app.route("/user_listing", methods=["POST"])
@jwt_required()
def user_listing():
    if request.method == "POST":
        userid = get_jwt_identity()
        otp_storage = OTPStorages.query.filter_by(userid=userid).first()
        if otp_storage.otp_verify == False:
            return jsonify({"result": False, "msg": "OTP가 인증되지 않았습니다."})
        otp_storage.otp_verify = False
        db.session.commit()
        user_list = list()
        users = Users.query.all()
        for user in users:
            user_dict = dict_stringfy(dict(user.__dict__))
            user_list.append(user_dict)
        return jsonify({"result": True, "users": user_list})
    return jsonify({"result": False, "msg": "HTTP Method Error"})


@app.route("/get_flag", methods=["POST"])
@jwt_required()
def get_flag():
    if request.method == "POST":
        userid = get_jwt_identity()
        otp_storage = OTPStorages.query.filter_by(userid=userid).first()
        if otp_storage.otp_verify == False:
            return jsonify({"result": False, "msg": "OTP가 인증되지 않았습니다."})
        otp_storage.otp_verify = False
        db.session.commit()
        user = Users.query.filter_by(userid=userid).first()
        if user.is_admin == False:
            return jsonify({"result": False, "msg": "관리자 권한이 아닙니다."})
        with open("flag.txt") as f:
            flag = f.read(32)
        return jsonify({"result": True, "msg": f"DH{{{flag}}}"})
    return jsonify({"result": False, "msg": "HTTP Method Error"})


def Setup():
    with app.app_context():
        if Users.query.filter_by(userid="admin").count() == 0:
            admin_user = Users(userid='admin', userpw='[DELETED]', is_admin=True) # sha256
            db.session.add(admin_user)
            db.session.commit()
        if OTPStorages.query.filter_by(userid="admin").count() == 0:
            admin_otp = OTPStorages(userid='admin', otp_secret='[DELETED]', otp_verify = False) # base32
            db.session.add(admin_otp)
            db.session.commit()


def dict_stringfy(dictionary):
    return dict(map(lambda x: (x[0], str(x[1])), dictionary.items()))


if __name__ == "__main__":
    Setup()
    app.run(host="0.0.0.0", port=8000)

 

기능 상으로 추측가능한 것 말고 특별히 기능을 우회할 만한 코드는 보이지 않는다.

다만 전역 변수로 {OTP_VALID_SEC = 120  # 2 minutes} 가 지정되어 있고,

OTP 생성 시 OTP를 유지하는 시간으로 확인된다.

OTP의 인증코드 길이는 6자의 숫자이므로 2분동안 무작위 대입 공격을 통해 뚫을 수도 있을 것 같다.

한 번 시도해보자.

 

1. 플래그 획득 페이지에서 임의의 OTP 인증 코드를 입력하고 인증하는 패킷을 캡처한다.

2. 인증코드 6자리를 무작위 대입한다.

3. 뚫릴때까지 기도한다.

 

 

아니 경우의 수가 1,000,000인데 이게 2분마다 초기화 된다고 생각해봐라. 아찔하다.

두 문장을 적는데 이제 고작 1400개의 패킷이 날아갔을 뿐이다. 이건 말도 안된다.

이대로는 안된다. 쓰레드를 5에서 20으로 늘려서 다시 진행해보자.

 

 

아까보다는 확실히 빠른 것 같다. 마음이 조금 편안해졌다.

이제 내가 할 수 있는건 기도 뿐이다. 기도하자.

 

오늘 기도빨이 잘 안받는다...ㅎㅎ

다시...

'워게임 > dreamhack' 카테고리의 다른 글

[dreamhack] csrf-2 문제풀이  (0) 2021.08.30
[dreamhack] xss-2 문제풀이  (0) 2021.08.30
[dreamhack] weblog-1 문제풀이  (0) 2021.07.22
[dreamhack] blind-command 문제풀이  (0) 2021.07.21
[dreamhack] web-ssrf 문제풀이  (0) 2021.07.21
복사했습니다!