cakeCTF Writeup

🌈 INFO
2022.09.03일 토요일에 진행한 cakeCTF writeup이다.중앙대학교 동아리에 ctf를 경험해보자고 공지글을 올려 팀원을 모았다. 시스템 2명 웹 2명으로 진행한 ctftime의 첫 ctf. 결과는 처참했지만, 다른 사람들의 writeup을 보며 실력을 키워보고자 한다.

1. cakeGEAR (web)

📃 Problem
화면

index.php
<?php
session_start();
$_SESSION = array();
define('ADMIN_PASSWORD', 'f365691b6e7d8bc4e043ff1b75dc660708c1040e');
/* Router login API */
$req = @json_decode(file_get_contents("php://input"));
if (isset($req->username) && isset($req->password)) {
if ($req->username === 'godmode'
&& !in_array($_SERVER['REMOTE_ADDR'], ['127.0.0.1', '::1'])) {
/* Debug mode is not allowed from outside the router */
$req->username = 'nobody';
}
switch ($req->username) {
case 'godmode':
/* No password is required in god mode */
$_SESSION['login'] = true;
$_SESSION['admin'] = true;
break;
case 'admin':
/* Secret password is required in admin mode */
if (sha1($req->password) === ADMIN_PASSWORD) {
$_SESSION['login'] = true;
$_SESSION['admin'] = true;
}
break;
case 'guest':
/* Guest mode (low privilege) */
if ($req->password === 'guest') {
$_SESSION['login'] = true;
$_SESSION['admin'] = false;
}
break;
}
/* Return response */
if (isset($_SESSION['login']) && $_SESSION['login'] === true) {
echo json_encode(array('status'=>'success'));
exit;
} else {
echo json_encode(array('status'=>'error'));
exit;
}
}
?>
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8">
<title>login - CAKEGEAR</title>
<style>input { margin: 0.5em; }</style>
</head>
<body style="text-align: center;">
<h1>CakeWiFi Login</h1>
<div>
<label>Username</label>
<input type="text" id="username" required>
<br>
<label>Password</label>
<input type="text" id="password" required>
<br>
<button onclick="login()">Login</button>
<p style="color: red;" id="error-msg"></p>
</div>
<script>
function login() {
let error = document.getElementById('error-msg');
let username = document.getElementById('username').value;
let password = document.getElementById('password').value;
let xhr = new XMLHttpRequest();
xhr.addEventListener('load', function() {
let res = JSON.parse(this.response);
if (res.status === 'success') {
window.location.href = "/admin.php";
} else {
error.innerHTML = "Invalid credential";
}
}, false);
xhr.withCredentials = true;
xhr.open('post', '/');
xhr.send(JSON.stringify({ username, password }));
}
</script>
</body>
</html>
admin.php도 주어져 있는데, session['admin']이 true라면 플래그를 보여주겠다는 내용이다.
✨ History
여러명이서 붙잡았던 이 문제, 이 문제에서 우리가 뚫을 방법으로 생각한 것은 3군데가 있었다.
- admin의 비밀번호를 sha1로 변환한 문자열을 노출시킨 것
- !in_array($_SERVER['REMOTE_ADDR'], ['127.0.0.1', '::1'])를 우회하는 것
- $req->username === 'godmode'를 우회하는 것
다만, 3번의 방법은 도저히 모르겠어서 1번과 2번을 붙잡았다.
- github의 admin 비밀번호 리스트로 사전 공격 → 실패
- http header 변경, 패킷을 여러 개를 보내 간섭하려는 방법 → 실패
그런데,, 결국 Exploit은 3번에서 발생했다.
🔑 Key Point
이것만 알면 문제 해결 가능!!
- php에서 case문은 ==으로 검사한다.
- ==의 우회 방법!
🔓 Exploit

php의 공식 문서에서 switch가 느슨한 비교(==)를 수행한다는 사실을 알 수 있다. 다만, godmode를 nobody로 바꾸는 부분에서는 엄격한 비교(===)를 수행함으로 우회할 수 있다.

이것은 ==에 대한 표이다. 여기서 되게 신기한 점이 있는데, true와 false 두 변수만을 이용해서 모든 == 조건을 통과할 수 있다는 것이다. 특히, true는 문자열과 비교할 때 true를 반환하니 이 점도 알아두면 좋겠다. 이번 문제의 취약점도 true와 문자열의 비교가 true를 반환한다는 것에 있다.
플래그를 얻어내는 방법은 아주 간단하다. godmode를 사용하는 것 → === 'godmode'에서는 거짓, == 'godmode'에서는 참을 반환하는 값을 찾아내는 것. 그것은 true이다.

burp suite로 패킷을 가로채 username에 true를 입력하면

플래그 값이 나온다.
2. readme 2022 (misc)

📃 Problem
server.py
import os
try:
f = open("/flag.txt", "r")
except:
print("[-] Flag not found. If this message shows up")
print(" on the remote server, please report to amdin.")
if __name__ == '__main__':
filepath = input("filepath: ")
if filepath.startswith("/"):
exit("[-] Filepath must not start with '/'")
elif '..' in filepath:
exit("[-] Filepath must not contain '..'")
filepath = os.path.expanduser(filepath)
try:
print(open(filepath, "r").read())
except:
exit("[-] Could not open file")
docker
FROM python:3.10-slim-buster
ENV DEBIAN_FRONTEND noninteractive
RUN apt-get update && apt-get install -yqq socat
RUN groupadd -r ctf && useradd -r -g ctf ctf
RUN echo "FakeCTF{**** REDUCTED ****}" > /flag.txt
RUN chmod 444 /flag.txt
USER ctf
WORKDIR /app
ADD server.py .
CMD socat TCP-L:9999,fork,reuseaddr EXEC:"python server.py",pty,ctty,stderr,echo=0
✨ History
/flag.txt라는 위치를 주는데, 뒤로 가는 방법들을 막은 문제. ..과 가장 처음의 /이 막혔다. 이 문자를 우회하려고 엄청 노력을 많이 했었는데, 이것을 우회하는 방법은 없었나보다. 코드의 특이한 점으로는 os.path.expanduser함수가 쓰였다는 점을 들 수 있다. 다음 내용을 찾아보니 다음과 같은 내용이 나왔다.

~을 이용하여 홈 디렉토리에 접근할 수 있게 해주는 함수라고 한다.

colab에서 코드를 짜서 확인해봤을 때, ~/가 colab의 홈 디렉토리인 /root/로 변환되는 것이 확인가능하다. 하지만 이 뿐, 홈 디렉토리 역시 최상위 경로는 아니었고 ..을 우회할 수 없어 실패했다.
🔑 Key Point
이것만 알면 문제 해결 가능!!
- os.path.expanduser함수의 또 다른 기능!
- 파일의 디렉터리 구조
- fd
- passwd
🔓 Exploit

os.path.expanduser에 관한 설명에서 번역체라 보지 못한 부분이 있었다. 사실 이 함수는 ~이후에 /가 아닌 user의 값을 넣으면 passwd에서 조회되는 그 user의 홈 디렉터리로 변환한다. 하지만 그럼에도 최상위 경로로는 이동할 수 없다. 여기서 이용할 수 있는 다른 개념이 fd이다. server.py함수는 앞부분에서 flag.txt를 open한다. 코드의 진행을 볼 때 여기서 open된 flag.txt는 프로그램이 끝날 때까지 닫히지 않는다. 이 의미는 fd 파일에 flag.txt 파일이 담겨있는 상태라는 것이다.

fd 파일은 dev 폴더에서 확인할 수 있다. 0,1,2가 각각 stdin, stdout, stderr인 것을 고려하면 flag.txt 파일은 이 이외의 fd 파일에 들어있겠다. 즉, dev 폴더의 접근으로 flag.txt 파일을 읽을 수 있다.
그렇다면 어떻게 dev 파일을 접근할까, os.path.expanduser의 기능이 passwd에서 조회한다는 점을 이용하면 된다.

etc 폴더에 있는 passwd 파일에는 user들의 정보가 들어가 있다. : 를 구문자로 순서대로 계정, 가려진 패스워드, UID, GID, GECOS, 홈 디렉터리, 로그인 때 실행하는 shell 프로그램이다. 우리가 봐야하는 부분은 홈 디렉터리를 dev로 사용하는 sys 사용자이다. 이 점을 이용해 우리는 fd 파일에 접근할 수 있다.


3. nimrev (rev)

📃 Problem
화면

✨ History
파일의 함수들이나 형식이 내가 알던 것과는 달라 엄청 시행착오를 겪었다. ida로 flag도 검색해보고 그랬지만,, 답은 나오지 않았다. 그러던 중 gdb를 통해 wrong이 출력되는 부분을 찾다가 플래그를 얻었다.

Exploit에서는 시행 착오 없이 빠르게 플래그를 얻는 법을 설명할 것이다. 문제에 대한 이해가 없더라도 이런게 리버싱이다 정도로 봐주면 좋겠다.
🔑 Key Point
이것만 알면 문제 해결 가능!!
- 비밀번호 있는 실행 파일은 이렇게 !! (
뇌피셜) - ida와 gdb 디버깅 도구 사용법
🔓 Exploit

실행파일을 실행시키면 Wrong...이 나온다. 이 말은 프로그램 상에 Wrong...이라는 문자가 저장되어 있다는 뜻이며 그러한 문자가 사용된다는 뜻이겠다. 그 위치를 ida로 찾아낼 수 있다.

ida에서 실행파일을 로드시킨 후 View → Open subviews 옵션에서 String를 찾을 수 있다. (만약 이 곳에 없다면 Names도 찾아보자.)

Wrong...문자열을 찾아냈다. 더블 클릭을 통해 메모리에 들어있는 문자열을 확인하자.

이곳에서 문자열을 확인할 수 있는데, 문자열의 시작 부분에 ; DATA XREF: 라는 주석처리된 문자가 보일 것이다. ida에서는 특정 문자열을 코드에서 찾아주는 기능을 제공한다. 사진에서 보이는 TM__~~~ 부분을 마우스 클릭하고 x를 누르면 문자열이 사용된 코드를 보여준다.

코드 부분으로 이동 후 f5를 눌러 디컴파일까지 하면 성공이다.

[ida를 통한 문자열 찾기 (3)] 그림을 참고할 때 코드의 56번째 줄이 Wrong...문자열인가 보다. 여기서 문자열을 비교하는 eqStrings가 보인다. key가 무엇인지는 모르지만 이 함수를 gdb로 확인해보면 비교하는 두 문자열이 나올 것이다.

break 명령어로 eqStrings 함수에서 멈춘 후, run으로 실행시켰다. 입력 문자는 아무 필요 없으니 아무렇게나 입력헀다.

x64 환경에서 함수의 인자를 레지스터에 저장한다 이 점을 이용해 telescope 명령어를 통해 레지스터의 값을 확인하니 플래그 값이 나왔다. (x86 환경에서는 스택을 확인하면 될 것)
이 함수 호출 전에 stack에 플래그가 한 번 노출된 적이 있다. 내가 그걸 보고 풀었었는데, 이러한 노가다를 통한 방법도 있다.