Exploit problem/CTF

cakeCTF Writeup

code_blue 2022. 9. 7. 21:44

🌈 INFO

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

대회 시간 내내 한 문제라도 풀어보자던 그 ctf...


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군데가 있었다.

  1. admin의 비밀번호를 sha1로 변환한 문자열을 노출시킨 것
  2. !in_array($_SERVER['REMOTE_ADDR'], ['127.0.0.1', '::1'])를 우회하는 것
  3. $req->username === 'godmode'를 우회하는 것

다만, 3번의 방법은 도저히 모르겠어서 1번과 2번을 붙잡았다. 

  1. github의 admin 비밀번호 리스트로 사전 공격 → 실패
  2. http header 변경, 패킷을 여러 개를 보내 간섭하려는 방법 → 실패

그런데,, 결국 Exploit은 3번에서 발생했다.

 

🔑 Key Point

이것만 알면 문제 해결 가능!!
  1. php에서 case문은 ==으로 검사한다.
  2. ==의 우회 방법!

🔓 Exploit

https://www.php.net/manual/en/control-structures.switch.php

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

 

https://www.php.net/manual/en/types.comparisons.php#types.comparisions-loose

이것은 ==에 대한 표이다. 여기서 되게 신기한 점이 있는데, 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함수가 쓰였다는 점을 들 수 있다. 다음 내용을 찾아보니 다음과 같은 내용이 나왔다. 

https://docs.python.org/ko/3/library/os.path.html

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

colab을 이용해서 ~을 입력했을 때 어떤 변화가 일어나는지 확인했다.

colab에서 코드를 짜서 확인해봤을 때, ~/가 colab의 홈 디렉토리인 /root/로 변환되는 것이 확인가능하다. 하지만 이 뿐, 홈 디렉토리 역시 최상위 경로는 아니었고 ..을 우회할 수 없어 실패했다.

🔑 Key Point

이것만 알면 문제 해결 가능!!
  1. os.path.expanduser함수의 또 다른 기능!
  2. 파일의 디렉터리 구조 
  3. fd
  4. passwd

🔓 Exploit

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

dev 폴더 안에 있는 fd를 확인할 수 있다

fd 파일은 dev 폴더에서 확인할 수 있다. 0,1,2가 각각 stdin, stdout, stderr인 것을 고려하면 flag.txt 파일은 이 이외의 fd 파일에 들어있겠다. 즉, dev 폴더의 접근으로 flag.txt 파일을 읽을 수 있다.

그렇다면 어떻게 dev 파일을 접근할까, os.path.expanduser의 기능이 passwd에서 조회한다는 점을 이용하면 된다.

passwd 파일이다

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

~sys를 통해 fd 디렉토리에 접근할 수 있다
fd/6 파일을 열어 flag 값을 얻어냈다


3. nimrev (rev)

📃 Problem

화면

123을 입력하니 Wrong...이 출력되었다

✨ History

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

얼떨결에 얻은 플래그

Exploit에서는 시행 착오 없이 빠르게 플래그를 얻는 법을 설명할 것이다. 문제에 대한 이해가 없더라도 이런게 리버싱이다 정도로 봐주면 좋겠다.

🔑 Key Point

이것만 알면 문제 해결 가능!!
  1. 비밀번호 있는 실행 파일은 이렇게 !! (뇌피셜)
  2. ida와 gdb 디버깅 도구 사용법

🔓 Exploit

123을 입력하니 Wrong...이 출력되었다

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

ida를 통한 문자열 찾기 (1)

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

ida를 통한 문자열 찾기 (2)

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

ida를 통한 문자열 찾기 (3)

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

ida를 통한 문자열 찾기 (4)

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

ida를 통한 문자열 찾기 (5)

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

gdb를 통한 flag 찾기 (1)

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

gdb를 통한 flag 찾기 (2)

x64 환경에서 함수의 인자를 레지스터에 저장한다 이 점을 이용해 telescope 명령어를 통해 레지스터의 값을 확인하니 플래그 값이 나왔다. (x86 환경에서는 스택을 확인하면 될 것)

 

이 함수 호출 전에 stack에 플래그가 한 번 노출된 적이 있다. 내가 그걸 보고 풀었었는데, 이러한 노가다를 통한 방법도 있다.