[Dreamhack] TODO List 0.0.1


✏️ 풀이

문제에 접속해보면 로그인과 회원가입 기능이 존재한다.

image-20250602173924191


이번 문제에서는 소스 코드가 많아서 핵심 소스 코드만 살펴볼 예정이다.

image-20250602202015252


create.sql 소스 코드를 보면, Users, Todolist 테이블에는 admin 계정과 데이터가 존재하고, Todo 테이블에서는 flag가 있다.

INSERT INTO Users (username, email, password) VALUES (
    'admin',
    'admin@dreamhack.io',
    'helloworld' -- redacted
);
INSERT INTO Todolist (user_id, name) VALUES (
    1,
    'admin'
);

INSERT INTO Todo (todo_list_id, title, description, is_completed) VALUES (
    1,
    'flag',
    'DH{sample_flag}',
    1
);


index.vue 소스 코드를 보면 중간에 shareTodo 버튼이 주석 처리가 되어있는 것을 알 수 있다.

<template>
  <div class="container">
    <div v-if="isLoggedIn" class="top-right">
      <button class="logout-button" @click="logout">Logout</button>
    </div>
    <div v-if="isLoggedIn">
      <h1>Your Todo Lists</h1>
      <button @click="toggleAddTodo">+ Add Todo</button>
      <div v-if="showAddForm">
        <input type="text" v-model="newTodo.title" placeholder="Title" required>
        <textarea v-model="newTodo.description" placeholder="Description" required></textarea>
        <input type="date" v-model="newTodo.dueDate">
        <button @click="addTodo">Submit</button>
      </div>
      <ul v-if="todoLists.length">
        <li v-for="item, idx in todoLists" :key="idx">
          <span :class="{ completed: item.is_completed }">: </span>
          <input type="checkbox" v-model="item.is_completed" @change="updateTodoStatus(item)">
          <!--
          under construction 
          <button @click="shareTodo"> Share </button>
          -->
        </li>
      </ul>
      <p v-else>No Todo Lists found.</p>
    </div>
    <div v-else>
      <h1>Welcome to Our Todo App</h1>
      <p>You are not logged in.</p>
      <button class="button" @click="navigateToLogin">Login</button>
      <button class="button" @click="navigateToSignup">Sign Up</button>
    </div>
  </div>
</template>


계정을 생성하여 패킷을 확인해보면, 아래와 같이 admin 계정의 userId 가 1번이므로 2번으로 생성된 것을 확인할 수 있다.

image-20250602211049722


Todo도 생성하여 확인해보면 id 1번은 flag 값이 있어 2번으로 생성된 것을 알 수 있다.

image-20250602211328971


shareTodo 기능은 버튼이 활성화 되어 있지 않지만, 기능이 있는 것으로 보아 flag 값이 있는 admin 계정의 Todo를 공유해서 flag 값을 획득해야할 것 같다. 아래 ShareTodo 함수를 살펴보면 /api/shareTodo 경로에서 POST 메소드로 data를 JSON 형태로 보내는 기능이 존재한다.

async function shareTodo(todo) {
  try {
    const data = JSON.stringify(
      todo
    );
    const token = localStorage.getItem('auth_token');
    if (!token) {
      throw new Error('login plz');
    }
    const response = await fetch('/api/shareTodo', {
      method: 'POST',
      headers: {
        'Content-Type': 'application/json',
        'Authorization': `Bearer ${token}`,
      },
      body: data,
    });
    if (!response.ok) {
      throw new Error('Failed to share api');
    }
  } catch (error) {
    console.error('Error share todo:', error);
    alert('Error share todo');
  }
}


todolist.js 에서는 userId를 이용해서 todoList를 가져오고, sharedList에 공유되어 있는 todo를 가져와 반환해서 보여주는 것 같다.

import { verifyToken } from '../utils/auth';
import { openDatabase } from '../utils/db';

export default defineEventHandler(async (event) => {
    try {
        const userData = verifyToken(event.req);
        const db = await openDatabase();
        const todoList = await db.all('SELECT * FROM Todo where todo_list_id=(SELECT id FROM Todolist WHERE Todolist.user_id = ?)', [userData.userId]);

        const sharedList = await db.all('SELECT * FROM TodoShares where user_id= ? ', [userData.userId]);
        for (const shared of sharedList){
            if (shared.permission_type === "owner" || shared.permission_type === "shared")
            todoList.push(await db.get('SELECT * FROM Todo where id = ?',[shared.todo_id]));
        };
        return todoList;
    } catch (error) {
        return createError({
            statusCode: 401,
            statusMessage: 'Unauthorized: ' + error.message
        });
    }
});


shareTodo.js 코드를 살펴보면, todo의 id를 통해서 is_completed가 되었는지 확인하고, 아니라면, id와 target_id를 받아와서 target_id에 있는 유저의 Todo List에 Todo를 공유해주는 것을 알 수 있다.

import { readBody, createError } from 'h3';
import { openDatabase } from '../utils/db';

export default defineEventHandler(async (event) => {
    const userData = verifyToken(event.req);
    const db = await openDatabase();
    const body = await readBody(event);
    
    const todo = body;
    try {
        const todo_data = await db.get(
            'SELECT * FROM Todo WHERE id = ?', [todo.id]
        );
        if (todo_data.is_completed === 1) {
            return { message: 'you cannot share already completed todo', id: todo_data.id}
        }

        const result = await db.run(
            `INSERT INTO TodoShares (todo_id, user_id, permission_type) VALUES
            (?, ?, ?)`,
            [todo_data.id, todo.target_id, 'shared']
          );
        return { success: true, message: 'Todo shared successfully', id: result.lastID };
    } catch (error) {
        throw createError({ statusCode: 500, statusMessage: 'Database error: ' + error.message });
    }
});


Todo List에서 체크 표시를 하면 burp에서 /api/updateTodo 경로로 value를 True라는 값으로 보낸다.

image-20250602213205694

image-20250602213220623


updateTodo.js 코드를 살펴보면, value와 id 값을 받아와서 is_completed 값을 수정해준다. 따라서 flag의 Todo 에서 updateTodo 경로를 이용해서 value의 값을 false로 바꿔보자.

import { readBody, createError } from 'h3';
import { openDatabase } from '../utils/db';

export default defineEventHandler(async (event) => {
    const userData = verifyToken(event.req);
    const db = await openDatabase();
    const body = await readBody(event);
    
    const { id, value} = body;
    try {
        const result = await db.run(
            `UPDATE todo
             SET is_completed = ?
             WHERE id = ?`,
            [value, id]
          );
        return { success: true, message: 'Todo updated successfully', id: result.lastID };
    } catch (error) {
        throw createError({ statusCode: 500, statusMessage: 'Database error: ' + error.message });
    }
});


flag 값의 Todo의 id는 1번이므로 아래와 같이 id 파라미터를 추가해서 value의 값을 false로 보내면 update가 잘 된 것을 확인할 수 있다.

image-20250602213513368


shareTodo에서는 아래와 같이 id와 target_id를 받아오므로, 아래처럼 /api/shareTodo 경로로 flag의 id는 1번이고, 공유할 유저의 id는 2번이므로 아래와 같이 설정할 수 있다.

    try {
        const todo_data = await db.get(
            'SELECT * FROM Todo WHERE id = ?', [todo.id]
        );
        if (todo_data.is_completed === 1) {
            return { message: 'you cannot share already completed todo', id: todo_data.id}
        }

        const result = await db.run(
            `INSERT INTO TodoShares (todo_id, user_id, permission_type) VALUES
            (?, ?, ?)`,
            [todo_data.id, todo.target_id, 'shared']
          );
        return { success: true, message: 'Todo shared successfully', id: result.lastID };
    } catch (error) {
        throw createError({ statusCode: 500, statusMessage: 'Database error: ' + error.message });
    }

image-20250602213729347


2번 계정으로 flag Todo를 공유했으므로, test 계정에 들어가 Todo를 다시 확인해보면 flag 값을 확인할 수 있다.

image-20250602213900829

댓글남기기