해당 대회는 사정상 참여를 못해서 대회 종료 후 문제를 풀어봤다.
먼저 문제에서 제공해주는 페이지에 들어가보면 간단한 패스워드 입력창만 있고 따로 동작을 안한다. 근데 소스를 보면 주석에 아래와 같은 구문이 있다.
해당 구문을 참조해 github에서 Kits-AB를 찾아보면 문제 사이트의 소스가 공개되어 있다.
app.py
import os
from model import init_database, Post
from flask import render_template, Flask, send_from_directory, request
init_database()
app = Flask(__name__, static_folder='static')
@app.route('/robots.txt')
def static_from_root():
return send_from_directory(app.static_folder, request.path[1:])
@app.route("/")
def index():
return render_template("login.html")
@app.route("/boards/<id>")
def board(id):
posts = []
if int(id) == 1:
posts = Post.select()
return render_template("board.html", posts=posts)
if __name__ == "__main__":
if os.environ.get("FLASK_DEBUG"):
app.run(debug=True)
else:
app.run()
메인 코드인데 보면, boards/1 경로에 접근하면 뭔가 데이터를 읽어서 뿌려주는걸 볼 수 있었다. 해당 페이지에 접근해보면 아래와 같이 여러 데이터들이 보이는데 그 중 암호화 된 값 하나가 보이는걸 볼 수 있었다.
해당 문제가 web+crypto+osint 였기 때문에 해당 암호문을 일단 디크립트 해야할 거라는 생각이 들었다. crypto는 공부를 거의 안해봐서 그만볼까 했는데 깃헙 소스에서 디크립트 하는 코드를 아래와 같이 제공해줘서 좀 더 풀어봤다.
test_crypto.py
from cryptography.hazmat.backends import default_backend
from cryptography.hazmat.primitives.asymmetric import rsa, padding
from cryptography.hazmat.primitives import serialization, hashes
import base64
import unittest
# Maybe this could be used to encrypt the secret messages in the board?
# https://cryptography.io/en/latest/hazmat/primitives/asymmetric/rsa/
MESSAGE=b"Something secret"
PEM_PASSWORD=b'aVerySecretPassword'
PEM_PRIVATE_KEY=b"""-----BEGIN ENCRYPTED PRIVATE KEY-----
MIIFHTBXBgkqhkiG9w0BBQ0wSjApBgkqhkiG9w0BBQwwHAQIeJ8sEumQimECAggA
MAwGCCqGSIb3DQIJBQAwHQYJYIZIAWUDBAEqBBCoq4tCJ4RHgmF8/Ayi+gRMBIIE
wA/ByLKYec9EnYxdklKLK3nnilG17fYrEeXGhkRy0tHuxDDJFrvZXANyiakSnj/r
0Ly52heKxEkXYTQ8ohJR5Fezn8KXLYJVdvkJkAGURiVICPb10f1m7UwqakPSt4Hk
nwXZRXYDyiNyUoMgIdxxpaNvl0h6GotOaa/CvcACnozZxiZv3X7f9+0y7zKYA+i8
lM5qaiFjz06LdQ0+MvSxqpC0lKbEJTrTvd95TsdkwNppoQQXU4p/CiGtrRC3DmCd
YZCSLAm7mlVfpnP2wcN7rX3rPQtlb0LCiWbLw2DmKaAbgW4yiqP12+yX0cegxZPH
KuvBtqDOEODDhro/j/VBSizhZxB9xgpsd1ZVdmIUGHmsckEg0pmHcOmb+L3/UwCX
6WI5HMecRk2miNnjZt19YPAdJJ0CNURnqkRMKw5dhy1e3V2+W1K2ojICJj7gZaSh
Hclt3VbwbjAQNwPUU2kkJWCQFDAjLnEmOgZEzESuo58kt3WyurJbeC5H5irTRlaT
jP9jCOvbuE2P4JR5ErOx5wxbMhI+UEVdcuHYGXoyJKLatg+i8W82BV+RQA9d7Bmq
qKdEWtLCD0IT9eCCm//M6iZiVHuDGjxgZVfvzaU7yHMdZdVi5mKfxHeIcGyrolVu
LDsOrjZ9aHtgVycMGjpltYdhJpTlP3Z2Otby18H0bUv1ntsRBZdx2lle8A1Jre1n
10DH5Lx5rn7prJuj/IL1q/Z4lcDlkvHI6I0m/rauXyddGcUrINTTWq9ujQ8x09Gt
NbLeoMOLy39H55W/T7VO+ds1kEOObE5lYwh0Jo29LpHLlKKpKVx23IHBTjC5LEAV
4qynUw1BLK1klEClZp9AfTAfz5M9AjK50l3MEEwIW48eS3U6h137Of3QirMjiE82
iFANV3rOYdsmQAtDeWxx+N3sLv8kK8ANnr85Dj9QOXQJtAm9S7UZM9BrwIgmOuVL
9r9Pt5J8B0lAwPQ5+sxTfgPrd0FhZSZYzrelbp0ck4odSnXFK+ZL0E1VWIBXUtTd
oj5lFFs9U95vXU5szx17xB+IMd2KOKIirIEwCm3TIa58sMbhLxDJtWpqlFVztg/E
zBeD3dzvhJzitTzKvFYTrzbge+o3/dK2+yFbibE0VTAGV60ILoZq5kLVqYgihk8I
7UHLw7ugunteNLXBpB2QEvETGXhjPu82dqZFS4q+KQkIm6n6XCh1oe/CpLg08Zzh
fAWLBv1OSs/tL9cRUWhY0JxcksP6jZrhNgBzqmN4mIeQ8BfaVQbgEaD/r0c4HgS/
68dRofW02JsfaNy0qgtnsWIvAez/2gq4Sryo3NJMX0V5YogmNAWl4dsonXVE5Yss
mR/0xgLIRqKB2S32ycBjCg0BJNDJE8KSpWZHPTZxel5NQqvOUzfoc7fA2B01OhQJ
EGRgwpp+4kPEU4cZz0FUN7Yv7YRWdkVgd0BJVHVdwog1/mX3hz5SktYoU9mzuuEV
COm52E8EDJmH+eDDmOcFoXDx9rV8vcnf8AMDE1eGRxuF6YjrdsOEhaCBaQXdB+0f
S2eccZTxfvwVCsVUsy2WrWJ6+C1qG7g3vsFiKy72eWjZ1BE5k1KZ/AMxQRi4wraL
jmt95WyzLVitJ54jC6KqXZQ=
-----END ENCRYPTED PRIVATE KEY-----"""
ENCRYPTED_MESSAGE=('KA6I/Hu3sUWtPIvqmWEUHctAtDwWm7ZSg1GhTOwZMOgZhxi+WobWX+Q+J4Mym9zW9CwKZnILBi9tP'
'+fXkionJC3U4A7APl6MPjtbkSPTqB6BXPug57dOVH2bKoyGCOkb1Y7GGs/wIVCebDyRH8katXP99q80y8Mr7wzw'
'+xL7dNcn01Ho6xYZQlbakqJOl2UCorFGReOryGgNfhYxnHWmSDkQDtFBsB/RnexqftYLVrnPiStwALsoO8eYLsI'
'1wnI1kmr5acbAFcW1G/0x4EZ/iouVu0EYisgQ8GXcwoed3wgQhUdrFAmI6DcbElza6QveNXCSsIIwjLWpzI2NrwPjYg==')
DEFAULT_PADDING=padding.OAEP(
mgf=padding.MGF1(algorithm=hashes.SHA256()),
algorithm=hashes.SHA256(),
label=None
)
class TestEncryption(unittest.TestCase):
def test_encryption_decryption(self):
private_key = rsa.generate_private_key(
public_exponent=65537,
key_size=2048,
backend=default_backend()
)
ciphertext = private_key.public_key().encrypt(
MESSAGE,
DEFAULT_PADDING
)
plaintext = private_key.decrypt(
ciphertext,
DEFAULT_PADDING
)
self.assertEqual(plaintext, MESSAGE)
def test_generate_private_key(self):
private_key = rsa.generate_private_key(
public_exponent=65537,
key_size=2048,
backend=default_backend()
)
private_pem = private_key.private_bytes(
encoding=serialization.Encoding.PEM,
format=serialization.PrivateFormat.PKCS8,
encryption_algorithm=serialization.BestAvailableEncryption(PEM_PASSWORD)
)
# print(private_pem.decode())
def test_encryption(self):
private_key=serialization.load_pem_private_key(
PEM_PRIVATE_KEY,
password=PEM_PASSWORD,
backend=default_backend()
)
ciphertext = private_key.public_key().encrypt(MESSAGE, DEFAULT_PADDING)
encoded_cipher = base64.b64encode(ciphertext)
# print(encoded_cipher)
# print(encoded_cipher.decode())
def test_decryption(self):
private_key = serialization.load_pem_private_key(
PEM_PRIVATE_KEY,
password=PEM_PASSWORD,
backend=default_backend()
)
plaintext = private_key.decrypt(
base64.b64decode(ENCRYPTED_MESSAGE.encode("utf-8")),
DEFAULT_PADDING
)
#print(plaintext)
self.assertEqual(MESSAGE, plaintext)
if __name__ == '__main__':
unittest.main()
코드에서 private key랑 password가 하드코딩으로 박혀있길래 암호화 값만 바꿔서 돌려봤는데 패스워드가 틀렸는지 정상적으로 복호화가 안됬다.
해볼만한게 딱히 안떠올라서 그냥 dictionary attack으로 사전파일 받아다 브루트 포스 돌렸는데도 복호화가 안됬다.
여기서 뭐지하다 깃헙 소스를 좀더 보다보니 commits이 3번 있었던게 눈에 들어왔다.
3번의 커밋 중 removed the production key, luckily it was encrypted with a password … 라는 커밋이 눈에 들어왔고 확인해보니 private key가 변경되었던 걸 확인할 수 있었다.
변경 전 private key를 통해 아래 코드로 브포를 돌리니까 암호화된 값이 복호화가 되었다.
decrypt.py
from cryptography.hazmat.backends import default_backend
from cryptography.hazmat.primitives.asymmetric import rsa, padding
from cryptography.hazmat.primitives import serialization, hashes
import base64
import unittest
PEM_PRIVATE_KEY = b"""-----BEGIN ENCRYPTED PRIVATE KEY-----
MIIFLTBXBgkqhkiG9w0BBQ0wSjApBgkqhkiG9w0BBQwwHAQIF+TK17Q9CAsCAggA
MAwGCCqGSIb3DQIJBQAwHQYJYIZIAWUDBAEqBBCebicNIgfA441g2E3t3z/oBIIE
0OAMyvjZ8MaFDLJzuzDY3RWHP0IHWiHoCBNxPJWySon/tLXoizSbsj8EKtgA0MpE
vORC4QdnKg7bqplAAXfSIRli9Hb7RcuMpKv5buW3/Oh/th8NWWM9LOQOBAO0svlR
pJhA5hZSKEgEJMd1E77mjv29gHMEzRgXvAsTOZXhgbbtPnIkQGPXZq4hXyhy0VBt
9cCevKYLgVFahIARjejN+KErNiSN0f76mc62wunum+J6uGtk/HYZ00ZsFcf/0x7B
O/8hrFsliAg+2izNLVWy/+b1oCkuaMIEZ0zXjse3iZirSmWs6F5tFGh2w5lnJB1G
hJAqTjhHdvPWpwiyTw4nCG7+FDd3v1Ih+v8Qq9evlkYg1rdwh13ymGcfko3y7p2l
SuQsJ94i5NEv4acgIE70fqXrwzbSlc+QB5RtKexMj0NxWCySe9seLQP9fbCxp6Ci
a8mHS/4hF7hBbH984QJxy7aqt+U/xLQrKkkp2Lf0KYfthmiS13e7ZEtNSzd3dxZv
eVnDNSzEh/ty/+yt5bx58AlmhNigkaPX+KrTYt1KgQBrgYyk/YNEWK8GE0Sq/4KL
uEiIa0mpbn9je7szIA9egwjIqLWasBoG1HOb5dOu/azhVoM8mheEik/FQLHhgZlo
ZoFY8Rb3jO3Mv/sod1tQE6IteAkBsfXGT8QNaJHMAjmf96aNA8y0bStpHm1ZzpzW
qX3xcr6bDAt4olonDZ1DNTZh4AnSCnKM8LM6kwwY0r8q13EHJ2Ek6L0Vh+BiIeNw
7Q/jQ1thXzrYv9e5KU5TmvZAvtXoqcUCmI2ehnOq6xmir07g4tPQIHyolbY8EHw1
r/mb3me1+8lPdvjKSCM/LqI04h3GPkfnXWwPwlBL4sd5mnKRunLHcnLDu2AVRE+R
r8DvGGIMNr+LZjxZIdjhMraR6VSSTXX028Lamz40ZY9gn3vQWeIJAi0S7g/TW+TJ
RwXGW5gmLfbzlkzgvXPRPfjk9EeBtcS4Pj7q2QIrrAdZZFCC4z5uRGmMHC/tv2/p
IYpV2kClKcnNuPvQSreJXB18GJo1VJU/o78/Hi/cr1atiERM38gP1FYk08vcwjwT
Av62VWaTXsuAsOzS/fjmSsyAlv0LN8pNJ6j3uvk+bOrbKS4V7aM0oHDhLtlJThN5
dagcklxP1VgRAXQPdGUz1oEZzoKezPxq2mJCj8QAPZFkat5mRzbUum0aAr3Yn7Vq
KLGrILx8p4sToqfiKMnayU/QCpgifgJbMun9pSvdOC40b8xUIeuN0PlIkLueA4Mu
o4pbU2inYbC+vEB3c1fHaki+Z0+jUuHyIWtEBJOD6VNYx1LU3HY6T7eV8t/8oJxi
LZCxhon+/R9kEgJO0ofp0362pFm5i1V1afzjFMAhFK4khFNdZJ6rJLrymg1ueCsx
sxSv8x8EA/ZykDJs4M/E5eSiZI9ZmrCsIrUXZ7QGjguqHXnHi7wsO3RSa2c8Bl+t
+SYlmqK5U55yHZ23rJIS/XNIaMB+mX0CHnx/+rohABcueD7Hz7Q0OHP34NuPwK3x
NAx6x4Yfrw2SiYd0Nj15N8oexI+u6/tahCL2obap9S1Y7zibfNgJs4d2yi3F3A+w
Fe+whD+k+txSfs6w50MFgI4JG2Hu6dLtdQC5FSyOAYDJ
-----END ENCRYPTED PRIVATE KEY-----"""
ENCRYPTED_MESSAGE = ('rW+fOddzrtdP7ufLj9KTQa9W8T9JhEj7a2AITFA4a2UbeEAtV/ocxB/t4ikLCMsThUXXWz+UFnyXzgLgD9RM+2toOvWRiJPBM2ASjobT+bLLi31F2M3jPfqYK1L9NCSMcmpVGs+OZZhzJmTbfHLdUcDzDwdZcjKcGbwEGlL6Z7+CbHD7RvoJk7Ft3wvFZ7PWIUHPneVAsAglOalJQCyWKtkksy9oUdDfCL9yvLDV4H4HoXGfQwUbLJL4Qx4hXHh3fHDoplTqYdkhi/5E4l6HO0Qh/jmkNLuwUyhcZVnFMet1vK07ePAuu7kkMe6iZ8FNtmluFlLnrlQXrE74Z2vHbQ==')
DEFAULT_PADDING = padding.OAEP(
mgf=padding.MGF1(algorithm=hashes.SHA256()),
algorithm=hashes.SHA256(),
label=None
)
def test_decryption(password):
try:
private_key = serialization.load_pem_private_key(
PEM_PRIVATE_KEY,
password=password,
backend=default_backend()
)
plaintext = private_key.decrypt(
base64.b64decode(ENCRYPTED_MESSAGE.encode("utf-8")),
DEFAULT_PADDING
)
print("Find Password = " + password)
print("Find Decrypt Text = " + plaintext)
exit()
except:
tmp = 0
f = open("C:\Users\Administrator\Desktop/rockyou.txt","r")
i = 0
password = f.read()
pass_list = password.split("\n")
print len(pass_list)
for i in xrange(len(pass_list)):
test_decryption(pass_list[i])
Result
Find Password = falloutboy
Find Decrypt Text = Bank url: http://bankofsweden-01.pwn.beer
복호화된 url에 접근해보니 안되서 문제설명을 다시보니 url을 찾은다음 5000 포트로 접근하라는 문장이 있었다.
http://bankofsweden-01.pwn.beer:5000 경로로 들어가보니 아래와 같이 사이트가 하나 나왔다.
기능 중에 로그인 및 회원가입 기능이 있었는데 그냥 회원가입 후 로그인해보면 계정 활성화가 안되었다고 나왔다. 그래서 계정명으로 사용한 이메일로 활성화 메일이 오나 했는데 아무것도 오는게 없었다. 여기서 취약점을 터트려야하나 싶어서 보니 회원가입 할 때 is_active란 파라미터가 있었다. 해당 파라미터를 true로 세팅하고 회원가입해주면 로그인이 되었다.
로그인 후 사이트를 쭉 보다보면 export하는 기능을 사용할 때 아래와 같이 lfi가 터지는걸 볼 수 있었다.
대충 lfi로 아래 순서로 파일을 쭉쭉 leak하다보면 아래와 같이 app.py 파일에 플래그가 존재하는 걸 볼 수 있었다.
/proc/self/environ
/proc/self/cmdline
/home/bos/ctf/app.py