[WebGoat๐Ÿ] SQL Injection - mitigation 1

๊ฐ€์˜ยท2020๋…„ 12์›” 4์ผ
0
post-thumbnail

Immutable Queries

Immutable Queries๋Š” SQL ์ธ์ ์…˜์˜ ๊ฐ€์žฅ ์ข‹์€ ๋ฐฉ์–ด ๋ฐฉ๋ฒ•์ด๋‹ค. ์ด๋ฒˆ์‹œ๊ฐ„์—” Immutable Query์— ๋Œ€ํ•ด ์ž์„ธํžˆ ์•Œ์•„๋ณผ ๊ฒƒ์ด๋‹ค!

Static Queries

Immutable query์—๋Š” static query๊ฐ€ ์žˆ๋‹ค! ๊ทผ๋ฐ static query๊ฐ€ ๋ฌด์—‡์ธ์ง€ ์ •์˜ํ•˜๋Š” ๋งŽ์€ ๊ธ€๋“ค๋งˆ๋‹ค ๋œป์ด ๋‹ค๋ฅธ ๊ฒƒ๊ฐ™์•„์„œ ํ˜ผ๋ž€์Šค๋Ÿฌ์› ๋‹ค. ๋ณดํ†ต์€ ํฌ๊ฒŒ ๋‘ ๊ฐ€์ง€ ๊ฐœ๋…์„ ๋ถ€๋ฅผ ๋•Œ ์‚ฌ์šฉํ•˜๋Š” ๊ฒƒ ๊ฐ™๋‹ค.

  1. Stringํ˜• ๋ณ€์ˆ˜์— ๋‹ด์ง€ ์•Š๊ณ , ์ฝ”๋“œ ์‚ฌ์ด์— ์ง์ ‘ ๊ธฐ์ˆ ํ•œ SQL๋ฌธ์„ ๋งํ•จ
  2. ์• ํ”Œ๋ฆฌ์ผ€์ด์…˜์ด ์‹คํ–‰๋  ๋•Œ๋Š” ๋ณ€ํ•˜์ง€ ์•Š๋Š” ์ฟผ๋ฆฌ๋ฅผ ๋งํ•จ

์˜ˆ์‹œ๋ฅผ ๋ณด๋ฉด ์ดํ•ด๊ฐ€ ์‰ฝ๋‹ค. static query์˜ ์ฒซ ๋ฒˆ์งธ ์ •์˜๋ฅผ ์ดํ•ดํ•  ์ˆ˜ ์žˆ๋Š” ์˜ˆ์‹œ๋ฅผ ๊ฐ€์ ธ์™€๋ดค๋‹ค. ์• ์ดˆ์— Stringํ˜• ๋ณ€์ˆ˜์— ๋‹ด์ง€ ์•Š๊ณ  ์†Œ์Šค ์ฝ”๋“œ ๋‚ด์—์„œ SQL์„ ๋‹ค๋ฃฐ ์ˆ˜ ์žˆ๋Š” ์–ธ์–ด๊ฐ€ ๋งŽ์ง€ ์•Š๋‹ค. ๋‚˜๋Š” ์ด๋ฒˆ์— ์ฒ˜์Œ ๋ณธ ์–ธ์–ด์ธ pro*C ์ฝ”๋“œ ๋‚ด์—์„œ SQL์„ ์ง์ ‘ ๊ธฐ์ˆ ํ•œ ์˜ˆ์‹œ์ด๋‹ค.

int main()
{
   printf("์‚ฌ๋ฒˆ์„ ์ž…๋ ฅํ•˜์‹ญ์‹œ์˜ค : ");
   scanf("%d", &empno);
   EXEC SQL WHENAVER NOT FOUND GOTO notfound;
   EXEC SQL SELECT ENAME INTO :ename
            FROM   EMP
            WHERE  EMPNO = :empno;
   printf("์‚ฌ์›๋ช… : %s.\n", ename);

notfound:
   printf("%d๋Š” ์กด์žฌํ•˜์ง€ ์•Š๋Š” ์‚ฌ๋ฒˆ์ž…๋‹ˆ๋‹ค. \n", empno);
}

๋ง ๊ทธ๋Œ€๋กœ ์†Œ์Šค์ฝ”๋“œ ๋‚ด์— SQL ๊ตฌ๋ฌธ์„ ๋ฐ”๋กœ ๊ธฐ์ˆ  ํ•˜๋Š” ๊ฒƒ์ด๋‹ค. static 'SQL' ์ธ ๊ฒƒ์ด๋‹ค.


๊ทธ๋ ‡๋‹ค๋ฉด ๋˜ ๋‹ค๋ฅธ ์‚ฌ๋žŒ๋“ค์ด ์ •์˜ํ•œ static query, ์• ํ”Œ๋ฆฌ์ผ€์ด์…˜์ด ์‹คํ–‰๋  ๋•Œ๋Š” ๋ณ€ํ•˜์ง€ ์•Š๋Š” ์ฟผ๋ฆฌ๋Š” ๋ฌด์—‡์ผ๊นŒ?

What is static SQL? Static SQL is SQL statements in an application that do not change at runtime and, therefore, can be hard-coded into the application.

์œ„์˜ static sql์˜ ์ •์˜ ์ค‘ hard-coded ๋  ์ˆ˜ ์žˆ๋‹ค๋Š” ๋ง์ด ์ค‘์š”ํ•˜๋‹ค. ๋ง๊ทธ๋Œ€๋กœ ์• ํ”Œ๋ฆฌ์ผ€์ด์…˜์ด ๋งŒ๋“ค์–ด์งˆ ๋•Œ ๊ณ ์ •๋œ๋‹ค๋Š” ๋œป์ด๋‹ค. ์ƒ์ˆ˜๊ฐ™์€ ๋Š๋‚Œ์œผ๋กœ ์• ํ”Œ๋ฆฌ์ผ€์ด์…˜์ด ์‹คํ–‰๋  ๋•Œ๋Š” ๋ณ€ํ•˜์ง€ ์•Š๋Š” ์ฟผ๋ฆฌ์ด๋‹ค. ์•„๋ž˜ ์ฝ”๋“œ๋Š” JAVA์—์„œ hard-coded query๋ฅผ ์ž‘์„ฑํ•œ ์˜ˆ์‹œ์ด๋‹ค.


 public static void viewTable(Connection con) throws SQLException {
    // hard-coded query statement
    String query = "SELECT last_name, id, hashedPassword, department, salary from EMPOYEES";
    try (Statement stmt = con.createStatement()) {
      ResultSet rs = stmt.executeQuery(query);
      while (rs.next()) { // Get rows from table (EMPLOYEES)
      
        String last_name = rs.getString("last_name");
        String id = rs.getString("id");
        String hasedPassword = rs.getString("hashedPassword");
        String department = rs.getString("department");
        int salary = rs.getInt("salary");
        
        //Print row
        System.out.println(last_name + ", " + id + ", " + hasedPassword + ", " + department + ", " + salary);
      }
    } catch (SQLException e) {
      JDBCTutorialUtilities.printSQLException(e);
    }
  }

static SQL์˜ ๋‘๋ฒˆ์งธ ์˜๋ฏธ๋Š” ๊ทธ ๋ฐ˜๋Œ€ ๊ฐœ๋…๋„ ์ค‘์š”ํ•œ๋ฐ, ๊ทธ๊ฑด ๋ฐ”๋กœ dynamic SQL์ด๋ผ๊ณ  ํ•œ๋‹ค. dynamic SQL์€ ์ปดํŒŒ์ผ ์‹œ์ ์—์„œ static๊ณผ ๋‹ฌ๋ฆฌ ์‹คํ–‰ ์ค‘์— ์ฟผ๋ฆฌ ๋‚ด์šฉ์ด ๋ฐ”๋€” ์ˆ˜ ์žˆ๋‹ค. ๊ทธ๋ ‡๊ฒŒ ์žฌ๊ตฌ์„ฑ๋œ ์ฟผ๋ฆฌ๊ฐ€ ์‹คํ–‰๋˜๋Š” ๊ฒƒ์ด๋‹ค.

๋Œ€๋‹ค์ˆ˜์˜ ํ”„๋กœ๊ทธ๋žจ์„ ๋งŒ๋“ค ๋•Œ ์ฟผ๋ฆฌ๋ฌธ์ด ์‚ฌ์šฉ์ž์˜ ์ž…๋ ฅ์— ๋”ฐ๋ผ ๋‹ฌ๋ผ์ง€๋Š” ๋•Œ๊ฐ€ ๋งŽ๊ธฐ ๋•Œ๋ฌธ์—, dynamic SQL์€ ์ •๋ง ๋งŽ์ด ์‚ฌ์šฉ๋œ๋‹ค๊ณ  ๋ณด๋ฉด ๋œ๋‹ค.

์ด ๋•Œ String ๋ณ€์ˆ˜๋ฅผ ๊ทธ๋ƒฅ ๋”ํ•ด์„œ(append) ์“ฐ๋ฉด SQL Injection์˜ ๊ฐ€๋Šฅ์„ฑ์ด ์ƒ๊ธฐ๋ฏ€๋กœ Prepared Statement๋ฅผ ์‚ฌ์šฉํ•˜๋Š” ๊ฒƒ์„ ๊ถŒ๊ณ ํ•˜๊ณ  ์žˆ๋‹ค.

Prepared Statement (Parameterized Queries)

SQL Injection ์ทจ์•ฝ์ ์— ๋Œ€ํ•œ ๋Œ€์‘ ๋ฐฉ์•ˆ์œผ๋กœ ๊ถŒ๊ณ ๋˜๋Š” ์œ ๋ช…ํ•œ ๋ฐฉ๋ฒ• ์ค‘ ํ•˜๋‚˜์ธ Prepared Statement์™€ binding ๋ณ€์ˆ˜์— ๋Œ€ํ•ด ์•Œ์•„๋ณด์ž.

Prepared Statement๋Š” ์žฌ์‚ฌ์šฉ ์‹œ ํšจ์œจ์„ฑ์„ ๋†’์ด๊ธฐ ์œ„ํ•ด ๊ณ ์•ˆ๋œ query๋ฌธ์˜ ํ˜•ํƒœ๋ผ๊ณ  ํ•œ๋‹ค. ๊ทธ ์›๋ฆฌ๋ฅผ ์ดํ•ดํ•˜๊ธฐ ์œ„ํ•ด์„œ๋Š” prepared statement์˜ workflow๋ฅผ ์•Œ์•„์•ผ ํ•œ๋‹ค.

Typical workflow of Using a Prepared Statement

  1. Prepare: ์ฒ˜์Œ์— ์• ํ”Œ๋ฆฌ์ผ€์ด์…˜์ด statement 'template'์„ ๋งŒ๋“ค์–ด์„œ DBMS์— ๋ณด๋‚ธ๋‹ค. statement์— ํฌํ•จ๋˜๋Š” ์–ด๋–ค value๋“ค์€ ๊ฐ’์„ ์ •ํ•ด๋†“์ง€ ์•Š๊ณ  parameters, placeholder ๋˜๋Š” bind ๋ณ€์ˆ˜ ๋“ฑ์œผ๋กœ(์•„๋ž˜์˜ "?" ๊ฐ€ ๊ทธ ์˜ˆ์‹œ) ๋Œ€์‹  ์ฑ„์›Œ๋„ฃ์–ด ๋ณด๋‚ด๋Š” ๊ฒƒ์ด๋‹ค.
    INSERT INTO products (name, price) VALUES (?, ?);

  2. ๊ทธ ํ›„์— DBMS๊ฐ€ statement template์„ ์ปดํŒŒ์ผ(parsing, optimization, translaiton)์„ ํ•œ๋‹ค. ๊ทธ๋ฆฌ๊ณ  ์ปดํŒŒ์ผ ๊ฒฐ๊ณผ๋ฅผ ์‹คํ–‰์€ ํ•˜์ง€ ์•Š๊ณ , ๋‚˜์ค‘์— ๋ฐ”๋กœ ๋Œ๋ฆด ์ˆ˜ ์žˆ๊ฒŒ ๋ฏธ๋ฆฌ ์ €์žฅํ•ด๋‘”๋‹ค.

  3. Execute: ์• ํ”Œ๋ฆฌ์ผ€์ด์…˜์ด ์ •ํ•ด์ง€์ง€ ์•Š์•˜๋˜ value๋“ค์˜ ๊ฐ’์„ DBMS์— ์ œ๊ณตํ•˜๋ฉด! ๊ทธ๋•Œ์„œ์•ผ DBMS๊ฐ€ ์ด์ „์— ์ž์‹ ์ด ์ €์žฅํ•ด๋‘์—ˆ๋˜ statement์˜ binding ๋ณ€์ˆ˜ ์ž๋ฆฌ์— value๋ฅผ ๋„ฃ์–ด ์ฟผ๋ฆฌ๋ฅผ ์‹คํ–‰ํ•œ๋‹ค. ๊ทธ๋ฆฌ๊ณ  ๋‚˜์„œ ๊ทธ ๊ฒฐ๊ณผ๋ฅผ ์• ํ”Œ๋ฆฌ์ผ€์ด์…˜์—๊ฒŒ ๋ฐ˜ํ™˜ํ•ด์ฃผ๋Š” ๊ฒƒ์ด๋‹ค.

์•„๋ž˜ ์†Œ์Šค์ฝ”๋“œ๋Š” JAVA์—์„œ PreparedStatement ๊ฐ์ฒด๋ฅผ ์‚ฌ์šฉํ•˜์—ฌ prepared statement๋ฅผ ๊ตฌํ˜„ํ•œ ์˜ˆ์‹œ์ด๋‹ค.

// 1. last_name ์„ binding ๋ณ€์ˆ˜๋กœ ๋Œ€์‹  ํ•ด๋†จ๋‹ค.
String query = "SELECT * FROM users WHERE last_name = ?"; 
// 2. DBMS์— ๋„˜๊ฒจ precompile
PreparedStatement statement = connection.prepareStatement(query); 
// 3. ์ž๋ฃŒํ˜•์ด String์ธ ๋ณ€์ˆ˜์ด๋ฏ€๋กœ setString()์„ ์ด์šฉํ•ด value ์ „๋‹ฌ
statement.setString(1, accoutName); 
// 4. ์ฟผ๋ฆฌ ์‹คํ–‰ ๊ฒฐ๊ณผ ์–ป๊ธฐ
ResultSet results = statement.executeQuery(); 
PreparedStatement stmt;
ResultSet rs;
StrongBuffer SQLStmt = new StringBuffer();
SQLStmt.append("SELECT ENAME, SAL FROM EMP");
SQLStmt.append("WHERE EMPNO = ?");

stmt = conn.prepareStatement(SQLStmt.toString());
stmt.setLong(1, txtEmpno.value); // ์ž๋ฃŒํ˜•์ด Long์ธ ๋ณ€์ˆ˜์ด๋ฏ€๋กœ setLong()์„ ์ด์šฉํ•ด value ์ „๋‹ฌ
rs = stmt.executeQuery(); // ์ฟผ๋ฆฌ ์‹คํ–‰ ๊ฒฐ๊ณผ ์–ป๊ธฐ

Stored Procedures

Stored Procedures๋Š” DBMS๋ฅผ ๋‹ค๋ฃฐ ๋•Œ ์‚ฌ์šฉํ•˜๋Š” ๋ณต์žกํ•œ ์ฟผ๋ฆฌ๋ฌธ์„ ํ•จ์ˆ˜์ฒ˜๋Ÿผ ๋“ฑ๋กํ•ด๋‘๊ณ  ๊ฐ„ํŽธํ•˜๊ฒŒ ์‚ฌ์šฉํ•  ์ˆ˜ ์žˆ๋Š” ๋ช…๋ น์ด๋‹ค. ์‹ค์ œ ํ•จ์ˆ˜์ฒ˜๋Ÿผ ๋งค๊ฐœ๋ณ€์ˆ˜๋„ ๋ฐ˜์˜ํ•  ์ˆ˜ ์žˆ๊ธฐ ๋•Œ๋ฌธ์— ์•Œ์•„๋‘๋ฉด ํŽธ๋ฆฌํ•œ ๊ธฐ๋Šฅ์ด๋‹ค.

Safe stored Procedure (Microsoft SQL Server)

๊ณต๊ฒฉ์œผ๋กœ๋ถ€ํ„ฐ ์•ˆ์ „ํ•œ ์ €์žฅ ํ”„๋กœ์‹œ์ €์˜ ์˜ˆ์‹œ์ด๋‹ค.

CREATE PROCEDURE ListCustomers(@Country nvarchar(30))
AS
	SELECT city, COUNT(*)
	FROM customers
	WHERE country LIKE @Country GROUP BY city

EXEC ListCustomers 'USA'

Injectable Stored Procedure (same environment)

์ธ์ ์…˜์— ์ทจ์•ฝํ•œ ์ €์žฅ ํ”„๋กœ์‹œ์ €์˜ ์˜ˆ์‹œ๋‹ค.

CREATE PROCEDURE getUser(@lastName nvarchar(25))
AS
	DECLARE @sql nvarchar(255)
	SET @sql = 'SELECT * FROM users WHERe lastname = + @LastName + '
EXEC sp_executesql @sql

์ด๋ฒˆ์—๋Š” ์•Œ์•„๋‘ฌ์•ผํ•˜๋Š” ๊ฐœ๋…๋“ค์ด ๋งŽ์•˜๋‹ค!! ๊ณต๋ถ€๋ฅผ ๋งˆ์ณค์œผ๋‹ˆ ๋ฌธ์ œ๋ฅผ ํ’€์–ด๋ณด์ž.

Try it! Writing safe code

You can see some code down below, but the code is incomplte. Complete the code, so that is no longer vulnerable for an SQL injection! Use the classes and methods you have learned before.
The code has to retrieve the status of the user based on the name and the mail address of the user. Both the name and the mail are in the String format.

์ฝ”๋“œ๋ฅผ ๋ณด๋ฉด conn์ด๋ผ๋Š” ์ฐธ์กฐ๋ณ€์ˆ˜๊ฐ€ DriverManager ํด๋ž˜์Šค์˜ static ๋ฉ”์„œ๋“œ์˜ ๋ฐ˜ํ™˜๊ฐ์ฒด๋ฅผ ์ฐธ์กฐํ•˜๋Š” ๊ฒƒ ๊ฐ™๋‹ค. Connection ๊ฐ์ฒด๋ฅผ ๋ฐ˜ํ™˜ํ•˜๊ณ  ์žˆ์Œ์„ ์ด์šฉํ•ด ๋ฉ”์„œ๋“œ๋ฅผ ์ฐพ์•„๋ณด์ž.

JAVA ๋„ํ๋จผํŠธ๋ฅผ ์ฐพ์•„๋ณด๋‹ˆ ๊ธˆ๋ฐฉ ๋‚˜์™”๋‹ค.

์—ญ์‹œ DriverManager ํด๋ž˜์Šค์— ์ •์˜๋œ static ๋ฉ”์„œ๋“œ์ธ getConnection() ์ด์—ˆ๋‹ค. ๊ทธ๋ž˜์„œ ์ฒซ ๋ฒˆ์งธ ๋นˆ์นธ์˜ ๋‹ต์€ getConnection์ด ๋˜๊ฒ ๋‹ค.

๋‹ค์Œ ์ฝ”๋“œ๋Š” ๋งŽ์ด ๋ดค๋˜ ๊ฑฐ๋‹ค. ์œ„์˜ PreparedStatement ์˜ˆ์ œ์—์„œ ํ•ด๋ดค๋˜ ๋Œ€๋กœ, Connection ๊ฐ์ฒด์˜ prepareStatement()๋ผ๋Š” ๋ฉ”์„œ๋“œ๋ฅผ ์‚ฌ์šฉํ•˜์—ฌ DBMS๊ฐ€ ์ฟผ๋ฆฌ๋ฅผ precompileํ•˜๊ฒŒ ํ•˜๋Š” ์ฝ”๋“œ์ด๋‹ค. ๊ทธ๋ž˜์„œ ๋‘๋ฒˆ์งธ ๋นˆ์นธ์˜ ์ •๋‹ต์€ PreparedStatement [์ฐธ์กฐ๋ณ€์ˆ˜ ์ด๋ฆ„ ์•„๋ฌด๊ฑฐ๋‚˜] ๊ฐ€ ๋œ๋‹ค. (๋‚˜๋Š” ๋ณ€์ˆ˜ ์ด๋ฆ„์„ ps๋กœ ํ–ˆ๋‹ค.) ๊ทธ๋ฆฌ๊ณ  ์„ธ๋ฒˆ์งธ ๋นˆ์นธ์€ prepareStatement๊ฐ€ ๋˜๊ฒ ๋‹ค.

prepareStatement() ๋ฉ”์„œ๋“œ์˜ ๋งค๊ฐœ๋ณ€์ˆ˜๋กœ๋Š” ์šฐ๋ฆฌ๊ฐ€ ๋งŒ๋“ค๊ณ ์‹ถ์€ prepared statement๋ฅผ binding ๋ณ€์ˆ˜('?')๋ฅผ ์‚ฌ์šฉํ•ด ๋งŒ๋“  String ๋ณ€์ˆ˜์ด๊ธฐ ๋•Œ๋ฌธ์— ๋„ค๋ฒˆ์งธ, ๋‹ค์„ฏ๋ฒˆ์งธ ๋นˆ์นธ์€ ๋ฌผ์Œํ‘œ๋ฅผ ๋„ฃ์–ด์ฃผ๋ฉด ๋œ๋‹ค.

๋ฌธ์ œ์—์„œ name๊ณผ mail์€ String ๋ณ€์ˆ˜๋กœ ์ €์žฅ๋˜์–ด์žˆ๋‹ค๊ณ  ํ–ˆ์œผ๋‹ˆ, binding ๋ณ€์ˆ˜์— ๊ฐ’์„ ๋„ฃ์–ด์ฃผ๋Š” ๋ฉ”์„œ๋“œ์ธ getString() ์‚ฌ์šฉํ•ด parameter์— ๋ณ€์ˆ˜๋ช…์„ ๋„ฃ์–ด์ฃผ๋Š” ์ฝ”๋“œ๋ฅผ ์—ฌ์„ฏ๋ฒˆ์งธ, ์ผ๊ณฑ๋ฒˆ์งธ์— ์ž‘์„ฑํ•˜๋ฉด ๋!


Try it! Writing Safe Code 2

Now it is time to write your own code! Your task is to use JDBC to connect to a database and request data from it.

Requirements:
1. Connect to a database
2. Perform a query on the datatbase which is immune to SQL injection attacks
3. your query needs to contain at least one String parameter.

๋‘ ๋ฒˆ์งธ ๋ฌธ์ œ๋„ ๋น„์Šทํ•œ๋ฐ, ์ด๋ฒˆ์—๋Š” ๋นˆ์นธ์ด ์•„๋‹ˆ๋ผ ์•„์˜ˆ ์ฝ”๋“œ๋ฅผ ์Œฉ์œผ๋กœ ์ž‘์„ฑํ•˜์—ฌ ์ œ์ถœํ•˜๋Š” ๋ฌธ์ œ์˜€๋‹ค.

๊ทธ๋ž˜์„œ ๋ฌธ์ œ์—์„œ ์‹œํ‚ค๋Š” ๋Œ€๋กœ ์ฝ”๋“œ๋ฅผ ์งœ๋ดค๋‹ค.

try {
    // Connect to a database
    Connection conn = DriverManager.getConnection(DBURL, DBUSER, DBPW);
    // Make a query which is immune to SQL injection attacks
    PreparedStatement pstmt;
    pstmt = conn.prepareStatement("SELECT * FROM users WHERE user_name=?");
    pstmt.setString(1, user_name);
    // Perform a query
    pstmt.execute();
} catch (Exception e) {
    System.out.println("Error");
}

๊ทผ๋ฐ ์—๋Ÿฌ๊ฐ€ ๋œฌ๋‹ค. ์ˆ˜์—…์„ ๋“ค์–ด๋ณด๋‹ˆ ์›น๊ณ ํŠธ ์ƒ ๋ฌธ์ œ๋ผ์„œ ํ•˜๋“œ์ฝ”๋”ฉ์„ ํ•ด์ค˜์•ผ ๋ฌธ์ œ๊ฐ€ ํ’€๋ฆฐ๋‹ค๊ณ  ํ–ˆ๋‹ค. ๊ทธ๋ž˜์„œ setString()์˜ ํŒŒ๋ผ๋ฏธํ„ฐ๋ฅผ ๋ฐ”๊ฟ”์ฃผ๋‹ˆ ๋ฐ”๋กœ ๋ฌธ์ œ๊ฐ€ ํ’€๋ ธ๋‹ค๐Ÿค”


๋!


์ด๋ฒˆ์—๋Š” ๊ฐœ๋…์ด ๋˜๊ฒŒ ์–ด๋ ค์› ๋Š”๋ฐ ํฌ์ŠคํŒ…ํ•˜๋ฉด์„œ ์ œ๋Œ€๋กœ ๊ณต๋ถ€ํ•  ์ˆ˜ ์žˆ์—ˆ๋˜ ๊ฒƒ ๊ฐ™๋‹ค! ๊ทธ์— ๋น„ํ•ด ๋ฌธ์ œ๋Š” ๋„ˆ๋ฌด ์‰ฌ์› ์–ด์„œ ์กฐ๊ธˆ ์•„์‰ฝ๊ธฐ๋„ ํ•˜๋‹ค๐Ÿ™‚ ํ”ผ๊ณคํ•˜๋‹ค ํ”ผ๊ณคํ•ด

0๊ฐœ์˜ ๋Œ“๊ธ€