2011-05-08 1 views
21

웹 디렉토리 중 일부에서 이상한 PHP 파일을 발견했습니다. 그들은 스패머 배치 파일로 밝혀졌습니다.Perl CGI가 해킹 당했습니까? 하지만 모든 것을 올바르게하고 있습니다.

그들은 2006 년부터 내가 CGI 스크립트를 사용하여 고액 기부 캠페인을 운영하고있을 때가되었습니다. 그리고 파일은 스크립트의 쓰기 가능 디렉토리에 저장되었으므로 스크립트가 어떻게 든 악용되었을 수도 있습니다.

하지만 Perl "taint checking", strict 등을 사용하고 있으며 쿼리 데이터를 절대 셸 (절대로 셸을 호출하지 않습니다!)에 전달하거나 쿼리 데이터를 사용하여 OPEN에 대한 파일 경로를 생성하지 않습니다. .. 나는 스크립트에서 직접 지정한 파일 만 OPEN합니다. 나는 파일 내용으로 쿼리 파일 INTO 작성된 파일을 전달하지만, 내가 아는 한 위험하지 않습니다.

나는이 스크립트를 봤기 때문에 아무 것도 볼 수 없으며 모든 표준 Perl CGI 구멍을 연구했습니다. 물론, 그들은 어쨌든 내 호스팅 계정에 대한 암호를 얻을 수 있었지만,이 스크립트들이 CGI 스크립트의 데이터 디렉토리에 저장되었다는 사실로 인해 스크립트가 의심 스럽습니다. (또한, 어떻게 든 내 암호를 "어떻게 든"얻는 것은 훨씬 더 무서운 설명입니다.) 또한 그 시간 전 내 로그에는 "PayPal이 아닌 주소에서받은 경고, 경고 IPN"메시지가 많이 표시됩니다. 그래서 누군가가 최소한 이러한 스크립트를 해킹하려고 시도한 것 같습니다.

두 개의 스크립트가 관련되어 있으며 아래에 붙여 넣습니다. 누구든지 예기치 않은 파일을 작성하기 위해 악용 될 수있는 것을 본 적이 있습니까?

#!/usr/bin/perl -wT 


# Created by Jason Rohrer, December 2005 
# Copied basic structure and PayPal protocol code from DonationTracker v0.1 


# Script settings 



# Basic settings 

# email address this script is tracking payments for 
my $receiverEmail = "receiver\@yahoo.com"; 

# This script must have write permissions to BOTH of its DataDirectories. 
# It must be able to create files in these directories. 
# On most web servers, this means the directory must be world-writable. 
# ( chmod a+w donationData ) 
# These paths are relative to the location of the script. 
my $pubDataDirectory = "../goliath"; 
my $privDataDirectory = "../../cgi-data/donationNet"; 

# If this $privDataDirectory setting is changed, you must also change it below 
# where the error LOG is opened 

# end of Basic settings 





# Advanced settings 
# Ignore these unless you know what you are doing. 



# where the log of incoming donations is stored 
my $donationLogFile = "$privDataDirectory/donationLog.txt"; 


# location of public data generated by this script 
my $overallSumFile = "$pubDataDirectory/overallSum.html"; 
my $overallCountFile = "$pubDataDirectory/donationCount.html"; 
my $topSiteListFile =  "$pubDataDirectory/topSiteList.html"; 

# private data tracking which donation total coming from each site 
my $siteTrackingFile = "$privDataDirectory/siteTracking.txt"; 

# Where non-fatal errors and other information is logged 
my $logFile =   "$privDataDirectory/log.txt"; 



# IP of notify.paypal.com 
# used as cheap security to make sure IPN is only coming from PayPal 
my $paypalNotifyIP = "216.113.188.202"; 



# setup a local error log 
use CGI::Carp qw(carpout); 
BEGIN { 

    # location of the error log 
    my $errorLogLocation = "../../cgi-data/donationNet/errors.log"; 

    use CGI::Carp qw(carpout); 
    open(LOG, ">>$errorLogLocation") or 
     die("Unable to open $errorLogLocation: $!\n"); 
    carpout(LOG); 
} 

# end of Advanced settings 


# end of script settings 








use strict; 
use CGI;    # Object-Oriented CGI library 



# setup stuff, make sure our needed files are initialized 
if(not doesFileExist($overallSumFile)) { 
    writeFile($overallSumFile, "0"); 
} 
if(not doesFileExist($overallCountFile)) { 
    writeFile($overallCountFile, "0"); 
} 
if(not doesFileExist($topSiteListFile)) { 
    writeFile($topSiteListFile, ""); 
} 
if(not doesFileExist($siteTrackingFile)) { 
    writeFile($siteTrackingFile, ""); 
} 


# allow group to write to our data files 
umask(oct("02")); 



# create object to extract the CGI query elements 

my $cgiQuery = CGI->new(); 




# always at least send an HTTP OK header 
print $cgiQuery->header(-type=>'text/html', -expires=>'now', 
         -Cache_control=>'no-cache'); 

my $remoteAddress = $cgiQuery->remote_host(); 



my $action = $cgiQuery->param("action") || ''; 

# first, check if our count/sum is being queried by another script 
if($action eq "checkResults") { 
    my $sum = readTrimmedFileValue($overallSumFile); 
    my $count = readTrimmedFileValue($overallCountFile); 

    print "$count \$$sum"; 
} 
elsif($remoteAddress eq $paypalNotifyIP) { 

    my $donorName; 


    # $customField contains URL of site that received donation 
    my $customField = $cgiQuery->param("custom") || ''; 

    # untaint and find whitespace-free string (assume it's a URL) 
    (my $siteURL) = ($customField =~ /(\S+)/); 

    my $amount = $cgiQuery->param("mc_gross") || ''; 

    my $currency = $cgiQuery->param("mc_currency") || ''; 

    my $fee = $cgiQuery->param("mc_fee") || '0'; 

    my $date = $cgiQuery->param("payment_date") || ''; 

    my $transactionID = $cgiQuery->param("txn_id") || ''; 


    # these are for our private log only, for tech support, etc. 
    # this information should not be stored in a web-accessible 
    # directory 
    my $payerFirstName = $cgiQuery->param("first_name") || ''; 
    my $payerLastName = $cgiQuery->param("last_name") || ''; 
    my $payerEmail = $cgiQuery->param("payer_email") || ''; 


    # only track US Dollars 
    # (can't add apples to oranges to get a final sum) 
    if($currency eq "USD") { 

    my $status = $cgiQuery->param("payment_status") || ''; 

    my $completed = $status eq "Completed"; 
    my $pending = $status eq "Pending"; 
    my $refunded = $status eq "Refunded"; 

    if($completed or $pending or $refunded) { 

     # write all relevant payment info into our private log 
     addToFile($donationLogFile, 
       "$transactionID $date\n" . 
       "From: $payerFirstName $payerLastName " . 
       "($payerEmail)\n" . 
       "Amount: \$$amount\n" . 
       "Fee: \$$fee\n" . 
       "Status: $status\n\n");      

     my $netDonation; 

     if($refunded) { 
     # subtract from total sum 

     my $oldSum = 
      readTrimmedFileValue($overallSumFile); 

     # both the refund amount and the 
     # fee on the refund are now reported as negative 
     # this changed as of February 13, 2004 
     $netDonation = $amount - $fee; 
     my $newSum = $oldSum + $netDonation; 

     # format to show 2 decimal places 
     my $newSumString = sprintf("%.2f", $newSum); 

     writeFile($overallSumFile, $newSumString); 


     my $oldCount = readTrimmedFileValue($overallCountFile); 
     my $newCount = $oldCount - 1; 
     writeFile($overallCountFile, $newCount); 

     } 

     # This check no longer needed as of February 13, 2004 
     # since now only one IPN is sent for a refund. 
     # 
     # ignore negative completed transactions, since 
     # they are reported for each refund (in addition to 
     # the payment with Status: Refunded) 
     if($completed and $amount > 0) { 
     # fee has not been subtracted yet 
     # (fee is not reported for Pending transactions) 

     my $oldSum = 
      readTrimmedFileValue($overallSumFile); 
       $netDonation = $amount - $fee; 
     my $newSum = $oldSum + $netDonation; 

     # format to show 2 decimal places 
     my $newSumString = sprintf("%.2f", $newSum); 

     writeFile($overallSumFile, $newSumString); 

     my $oldCount = readTrimmedFileValue( 
          $overallCountFile); 
     my $newCount = $oldCount + 1; 
     writeFile($overallCountFile, $newCount); 
     } 

     if($siteURL =~ /http:\/\/\S+/) { 
     # a valid URL 

     # track the total donations of this site 
     my $siteTrackingText = readFileValue($siteTrackingFile); 
     my @siteDataList = split(/\n/, $siteTrackingText); 
     my $newSiteData = ""; 
     my $exists = 0; 
     foreach my $siteData (@siteDataList) { 
      (my $url, my $siteSum) = split(/\s+/, $siteData); 
      if($url eq $siteURL) { 
      $exists = 1; 
      $siteSum += $netDonation; 
      } 
      $newSiteData = $newSiteData . "$url $siteSum\n"; 
     } 

     if(not $exists) { 
      $newSiteData = $newSiteData . "$siteURL $netDonation"; 
     } 

     trimWhitespace($newSiteData); 

     writeFile($siteTrackingFile, $newSiteData); 

     # now generate the top site list 

     # our comparison routine, descending order 
     sub highestTotal { 
      (my $url_a, my $total_a) = split(/\s+/, $a); 
      (my $url_b, my $total_b) = split(/\s+/, $b); 
      return $total_b <=> $total_a; 
     } 

     my @newSiteDataList = split(/\n/, $newSiteData); 

     my @sortedList = sort highestTotal @newSiteDataList; 

     my $listHTML = "<TABLE BORDER=0>\n"; 
     foreach my $siteData (@sortedList) { 
      (my $url, my $siteSum) = split(/\s+/, $siteData); 

      # format to show 2 decimal places 
      my $siteSumString = sprintf("%.2f", $siteSum); 

      $listHTML = $listHTML . 
      "<TR><TD><A HREF=\"$url\">$url</A></TD>". 
      "<TD ALIGN=RIGHT>\$$siteSumString</TD></TR>\n"; 
     } 

     $listHTML = $listHTML . "</TABLE>"; 

     writeFile($topSiteListFile, $listHTML); 

     } 


    } 
    else { 
     addToFile($logFile, "Payment status unexpected\n"); 
     addToFile($logFile, "status = $status\n"); 
    } 
    } 
    else { 
    addToFile($logFile, "Currency not USD\n"); 
    addToFile($logFile, "currency = $currency\n"); 
    } 
} 
else { 
    # else not from paypal, so it might be a user accessing the script 
    # URL directly for some reason 


    my $customField = $cgiQuery->param("custom") || ''; 
    my $date = $cgiQuery->param("payment_date") || ''; 
    my $transactionID = $cgiQuery->param("txn_id") || ''; 
    my $amount = $cgiQuery->param("mc_gross") || ''; 

    my $payerFirstName = $cgiQuery->param("first_name") || ''; 
    my $payerLastName = $cgiQuery->param("last_name") || ''; 
    my $payerEmail = $cgiQuery->param("payer_email") || ''; 


    my $fee = $cgiQuery->param("mc_fee") || '0'; 
    my $status = $cgiQuery->param("payment_status") || ''; 

    # log it 
    addToFile($donationLogFile, 
      "WARNING: got IPN from unexpected IP address\n" . 
      "IP address: $remoteAddress\n" . 
      "$transactionID $date\n" . 
      "From: $payerFirstName $payerLastName " . 
      "($payerEmail)\n" . 
      "Amount: \$$amount\n" . 
      "Fee: \$$fee\n" . 
      "Status: $status\n\n"); 

    # print an error page 
    print "Request blocked."; 
} 



## 
# Reads file as a string. 
# 
# @param0 the name of the file. 
# 
# @return the file contents as a string. 
# 
# Example: 
# my $value = readFileValue("myFile.txt"); 
## 
sub readFileValue { 
    my $fileName = $_[0]; 
    open(FILE, "$fileName") 
     or die("Failed to open file $fileName: $!\n"); 
    flock(FILE, 1) 
     or die("Failed to lock file $fileName: $!\n"); 

    my @lineList = <FILE>; 

    my $value = join("", @lineList); 

    close FILE; 

    return $value; 
} 



## 
# Reads file as a string, trimming leading and trailing whitespace off. 
# 
# @param0 the name of the file. 
# 
# @return the trimmed file contents as a string. 
# 
# Example: 
# my $value = readFileValue("myFile.txt"); 
## 
sub readTrimmedFileValue { 
    my $returnString = readFileValue($_[0]); 
    trimWhitespace($returnString); 

    return $returnString; 
} 



## 
# Writes a string to a file. 
# 
# @param0 the name of the file. 
# @param1 the string to print. 
# 
# Example: 
# writeFile("myFile.txt", "the new contents of this file"); 
## 
sub writeFile { 
    my $fileName = $_[0]; 
    my $stringToPrint = $_[1]; 

    open(FILE, ">$fileName") 
     or die("Failed to open file $fileName: $!\n"); 
    flock(FILE, 2) 
     or die("Failed to lock file $fileName: $!\n"); 

    print FILE $stringToPrint; 

    close FILE; 
} 



## 
# Checks if a file exists in the filesystem. 
# 
# @param0 the name of the file. 
# 
# @return 1 if it exists, and 0 otherwise. 
# 
# Example: 
# $exists = doesFileExist("myFile.txt"); 
## 
sub doesFileExist { 
    my $fileName = $_[0]; 
    if(-e $fileName) { 
     return 1; 
    } 
    else { 
     return 0; 
    } 
} 



## 
# Trims any whitespace from the beginning and end of a string. 
# 
# @param0 the string to trim. 
## 
sub trimWhitespace { 

    # trim from front of string 
    $_[0] =~ s/^\s+//; 

    # trim from end of string 
    $_[0] =~ s/\s+$//; 
} 



## 
# Appends a string to a file. 
# 
# @param0 the name of the file. 
# @param1 the string to append. 
# 
# Example: 
# addToFile("myFile.txt", "the new contents of this file"); 
## 
sub addToFile { 
    my $fileName = $_[0]; 
    my $stringToPrint = $_[1]; 

    open(FILE, ">>$fileName") 
     or die("Failed to open file $fileName: $!\n"); 
    flock(FILE, 2) 
     or die("Failed to lock file $fileName: $!\n"); 

    print FILE $stringToPrint; 

    close FILE; 
} 



## 
# Makes a directory file. 
# 
# @param0 the name of the directory. 
# @param1 the octal permission mask. 
# 
# Example: 
# makeDirectory("myDir", oct("0777")); 
## 
sub makeDirectory { 
    my $fileName = $_[0]; 
    my $permissionMask = $_[1]; 

    mkdir($fileName, $permissionMask); 
} 

을 그리고, 미안 여기에 몇 가지 중복 (거기 ... :

여기 (가장 기부를 생성하는 사이트 추적 또한 페이팔 IPN를 받고 기부금을 추적하고 용) 첫 번째 스크립트입니다 ?) 완전성,하지만 여기에 두 번째 스크립트는 사람들이 자신의 사이트에 추가 할 수있는 웹 사이트의 HTML 버튼)을 생성 (이다 : 나는 펄의 CGI 모듈과 경기 이후

#!/usr/bin/perl -wT 


# Created by Jason Rohrer, December 2005 


# Script settings 



# Basic settings 

my $templateFile = "buttonTemplate.html"; 

# end of Basic settings 





# Advanced settings 
# Ignore these unless you know what you are doing. 

# setup a local error log 
use CGI::Carp qw(carpout); 
BEGIN { 

    # location of the error log 
    my $errorLogLocation = "../../cgi-data/donationNet/errors.log"; 

    use CGI::Carp qw(carpout); 
    open(LOG, ">>$errorLogLocation") or 
     die("Unable to open $errorLogLocation: $!\n"); 
    carpout(LOG); 
} 

# end of Advanced settings 


# end of script settings 








use strict; 
use CGI;    # Object-Oriented CGI library 


# create object to extract the CGI query elements 

my $cgiQuery = CGI->new(); 




# always at least send an HTTP OK header 
print $cgiQuery->header(-type=>'text/html', -expires=>'now', 
         -Cache_control=>'no-cache'); 


my $siteURL = $cgiQuery->param("site_url") || ''; 

print "Paste this HTML into your website:<BR>\n"; 

print "<FORM><TEXTAREA COLS=40 ROWS=10>\n"; 

my $buttonTemplate = readFileValue($templateFile); 

$buttonTemplate =~ s/SITE_URL/$siteURL/g; 

# escape all tags 
$buttonTemplate =~ s/&/&amp;/g; 
$buttonTemplate =~ s/</&lt;/g; 
$buttonTemplate =~ s/>/&gt;/g; 


print $buttonTemplate; 

print "\n</TEXTAREA></FORM>"; 




## 
# Reads file as a string. 
# 
# @param0 the name of the file. 
# 
# @return the file contents as a string. 
# 
# Example: 
# my $value = readFileValue("myFile.txt"); 
## 
sub readFileValue { 
    my $fileName = $_[0]; 
    open(FILE, "$fileName") 
     or die("Failed to open file $fileName: $!\n"); 
    flock(FILE, 1) 
     or die("Failed to lock file $fileName: $!\n"); 

    my @lineList = <FILE>; 

    my $value = join("", @lineList); 

    close FILE; 

    return $value; 
} 



## 
# Reads file as a string, trimming leading and trailing whitespace off. 
# 
# @param0 the name of the file. 
# 
# @return the trimmed file contents as a string. 
# 
# Example: 
# my $value = readFileValue("myFile.txt"); 
## 
sub readTrimmedFileValue { 
    my $returnString = readFileValue($_[0]); 
    trimWhitespace($returnString); 

    return $returnString; 
} 



## 
# Writes a string to a file. 
# 
# @param0 the name of the file. 
# @param1 the string to print. 
# 
# Example: 
# writeFile("myFile.txt", "the new contents of this file"); 
## 
sub writeFile { 
    my $fileName = $_[0]; 
    my $stringToPrint = $_[1]; 

    open(FILE, ">$fileName") 
     or die("Failed to open file $fileName: $!\n"); 
    flock(FILE, 2) 
     or die("Failed to lock file $fileName: $!\n"); 

    print FILE $stringToPrint; 

    close FILE; 
} 



## 
# Checks if a file exists in the filesystem. 
# 
# @param0 the name of the file. 
# 
# @return 1 if it exists, and 0 otherwise. 
# 
# Example: 
# $exists = doesFileExist("myFile.txt"); 
## 
sub doesFileExist { 
    my $fileName = $_[0]; 
    if(-e $fileName) { 
     return 1; 
    } 
    else { 
     return 0; 
    } 
} 



## 
# Trims any whitespace from the beginning and end of a string. 
# 
# @param0 the string to trim. 
## 
sub trimWhitespace { 

    # trim from front of string 
    $_[0] =~ s/^\s+//; 

    # trim from end of string 
    $_[0] =~ s/\s+$//; 
} 



## 
# Appends a string to a file. 
# 
# @param0 the name of the file. 
# @param1 the string to append. 
# 
# Example: 
# addToFile("myFile.txt", "the new contents of this file"); 
## 
sub addToFile { 
    my $fileName = $_[0]; 
    my $stringToPrint = $_[1]; 

    open(FILE, ">>$fileName") 
     or die("Failed to open file $fileName: $!\n"); 
    flock(FILE, 2) 
     or die("Failed to lock file $fileName: $!\n"); 

    print FILE $stringToPrint; 

    close FILE; 
} 



## 
# Makes a directory file. 
# 
# @param0 the name of the directory. 
# @param1 the octal permission mask. 
# 
# Example: 
# makeDirectory("myDir", oct("0777")); 
## 
sub makeDirectory { 
    my $fileName = $_[0]; 
    my $permissionMask = $_[1]; 

    mkdir($fileName, $permissionMask); 
} 
+1

제이슨이 해당 컴퓨터의 유일한 동적 내용입니까? –

+0

업로드 디렉토리의 파일이 모두 실행 가능하고 외부에서 접근 할 수있는 경우 문제가 발생합니다. 하지만 당신은 언급하지 않았습니다. 실행 가능합니까? 아니면 실패한 시도임을 나타내는 파일일까요? 물론 웹 서버는 업로드 디렉토리에서 실행 가능한 컨텐츠를 찾지 않아야합니다. 또한 누가 그 기계에 접근 할 수 있었습니까? 안에있는 사람이 될 수 있을까요? 기계가 최신 상태로 유지되지 않은 경우 – DavidO

+8

, 다음 오년 많은 시간은 기계와에서 실행중인 웹 서버에 봄하는 원격 취약점, 또는 메일 서버, 또는 커널, 또는 다른 것입니다 네트워크 액세스. – Quentin

답변

0

은 오랜만,하지만 당신은 확실히 CGI 있습니다 :: param이 값을 이스케이프합니까? 내가 앉아있는 곳에서 값에는 역 따옴표가 포함될 수 있으므로 확장되고 실행됩니다.

+2

나는 무제한의'open FIL E, $ filename' .. 어떻게 든'$ filename'을 조작 할 수 있다면, 당신이 원하는 것을 거의 할 수 있습니다. 'open FILE "| echo #!/usr/bin/perl> hack.cgi"' – TLP

+0

음 ...나는 사용자 제출 변수가 아무것도하지 못하게하는 backticks를 생각하지 않는다. 변수가 사용될 때 아무것도 확장되지 않는다. (이미 문자열이다.) 그렇지? 내가 알고있는 모든 익스플로잇은 사용자가 제출 한 변수를 OPEN 호출에 전달하거나 OWN 백틱에서 사용자가 제출 한 변수를 사용합니다. TLP, 당신은 OPEN 호출이 잠재적으로 문제가있을 수 있다는 것에 대해 옳았습니다. 그러나이 서브 루틴이 호출되는 곳을 보면 $ filename에 사용자가 제출 한 변수가 절대 포함되지 않습니다. 나만 하드 코딩 된 4 개 또는 5 개의 파일 이름의 고정 세트 만 열 수 있습니다. –

+3

'open'과 관련된 안전 예방 조치는 Perl의 버전이 지원한다고 가정 할 때 세 가지 인수 버전을 사용하는 것입니다. 나는 그것이 이미 10 년 이상 된 5.6.1에 나타 났을 것이라고 생각한다. 예 : FILE, '>', $ filename 또는 die $ !;를여십시오. 이렇게하면 파일 모드와 다른 매개 변수에 열린 모드 (>)가 나타나 쉘 주입을 방지합니다. 자세한 내용은 perldoc -f open을 참조하십시오. – DavidO

0

당신은 constant pragma로 컴파일 시간 상수로 모든 파일 경로 참조를 만드는 코드 리팩토링 수 : 그들은 qq을 통해 삽입되지 않기 때문에 통증이

use constant { 
    DIR_PRIVATE_DATA => "/paths/of/glory", 
    FILE_DONATION_LOG => "donationLog.txt" 
}; 

open(FILE, ">>".DIR_PRIVATE_DATA."/".FILE_DONATION_LOG); 

가 상수로 취급을하고 ' 에 끊임없이 이 있고 (s)printf 또는 많은 연결 연산자를 사용하고 있습니다. 그러나은 ne'erdowell이 파일 경로로 전달되는 모든 인수를 변경하는 것이 훨씬 더 어려워 야합니다.

+1

이러한 단점 때문에 [상수 모듈은 사용하지 않아야합니다.] (http://p3rl.org/Perl::Critic::Policy::ValuesAndExpressions::ProhibitConstantPragma). 추천 [Const :: Fast] (http://p3rl.org/Const::Fast). – daxim

2

이전과 비슷한 것을 보았습니다. 우리의 경우, 해커가 업데이트되지 않은 라이브러리에서 버퍼 오버 플로우를 사용했다고 확신합니다. 그런 다음 PHP 셸을 사용하여 파일을 서버에 쓸 수있었습니다.

코드에 문제가있는 것 같습니다. 소프트웨어를 자주 업데이트하면 공격 가능성이 줄어들지 만 안타깝게도 해킹을 완벽하게 방지하는 것은 불가능합니다. 이전 버전의 소프트웨어에서 일반적인 취약점을 검색 할 가능성이 있습니다.

0

코드가 내게 안전합니다.파일에 상대 경로를 사용하는 것에는 약간의 반대 만 할뿐입니다. 조금 불편한 점이 있지만 보안 위험을 상상하기는 어렵습니다. 나는 취약점이 (perl, apache ...) 어딘가에 있다고 내기했다.

0

거대한 문제가 아니라면 PROBABLY 그냥 그대로 둘 수있다. 그렇게 할 수 없다면 백업에서 복원하십시오. 너무 오랫동안 해킹당한 것을 보면서 아마 그렇게 할 수 없을 것입니다. 세 번째이고 가장 가능성있는 대답은 할 수있는 것을 백업하고 그냥 핵무기를 처음부터 다시 시작하는 것입니다. 처음부터 모든 것을 다시 수행하십시오.

관련 문제