เบื้องล่างของ OWASP — A1 Injection

Bank Eakasit
7 min readJul 17, 2018

ไม่ได้เขียนอะไรมานาน งานค่อนข้างเยอะ วันนี้เอาอะไรง่ายๆ เรื่อง Injection ที่ทุกคนน่าจะเคยได้ยินบ้างในชื่อ SQL injection. แต่ SQL injection นั้นเป็นแค่ส่วนหนึ่งของ set ที่ใหญ่กว่าที่เรียกว่า Injection ครับ.

บทความนี้ จะเป็นการวิเคราะห์เบื้องล่าง ว่าแท้จริงแล้ว Injection นั้นเกิดขึ้นได้อย่างไร ทำไมหลายคนถึงแนะนำ prepared statement ว่าเป็นการป้องกันที่ดีที่สุด และจะพูดไปถึง Injection แบบอื่นๆด้วยครับ เช่น Shell command injection, eval, etc.

สำหรับเหตุผลที่ผมเขียนบทความนี้ขึ้นมานั้น เพราะมีความรู้สึกอยากให้ผู้อ่าน ทั้งที่เป็น developer ทั่วไปและ framework/service dev นั้นมองเห็นถึง root cause ที่แท้จริงของมัน และมีการ design ระบบออกมาให้ secure มากขึ้น. เพราะปัจจุบันผมเห็นทุกคนก็เข้าใจ SQL injection บอกให้ใช้ prepared statement แล้วปลอดภัยเรียบร้อย แต่พอไปเขียนจุดอื่นที่คล้ายๆกัน ก็มีช่องโหว่ Injection โผล่มาอยู่ดี. โดยหวังว่าผู้ที่อ่านอันนี้แล้ว จะกลับไปเขียนโปรแกรม ไม่ใช่แค่ปลอดภัยจาก SQL injection แต่สามารถเขียนให้ตัวเองปลอดภัยจาก Injection แบบอื่นๆได้มากที่สุดด้วยครับ.

ผมจะพูดกว้างๆใน Injection ภาพรวมทั้งหมด ไม่ได้เจาะจงไปเฉพาะ SQL injection เท่านั้น แต่สุดท้ายหวังว่าผู้อ่านจะเห็น pattern ของมัน และเข้าใจว่าจริงๆ Injection แบบอื่นๆเนี่ย root cause มันก็ไม่ได้ต่างไปจาก SQL injection เลย.

สุดท้ายนี้ ถ้ามีอะไรผิดตรงไหน ทักมาให้แก้ได้ทันทีเลยนะครับ.

Injection เกิดจากอะไร

เริ่มจากให้ผู้อ่านดูโค้ดและ flow ตัวอย่างกันก่อนดีกว่า แล้วคำตอบนั้นจะชัดเจนมากขึ้นครับ (ขอเลือก PHP เนื่องจากตรงไปตรงมาชัดเจน แต่ภาษาอื่นก็ไม่ต่างกันครับ)

Case 1
$sql = "SELECT username FROM Users where username='$username' AND password='$password';"
$result = $conn->query($sql);
What if $username = ' or '1'='1 ?
Case 1 : เอา input user มาปั้น query แล้วยิงลง database ตรงๆ โดยไม่มี escape ใดๆ
Case 2
$query = "SELECT first_name, last_name FROM users WHERE user_id = (:id);"
$data = $db->prepare( $query );
$data->bindParam( ':id', $id, PDO::PARAM_INT );
$data->execute();
Ok. Injection go go away.
Case 2: SQL prepared statement / PDO
Case 3
$text = $_GET['text'];
echo($text);
What if $text = "First");echo("Twice" ?
Can we really inject some commands here ? ...We can't. Why ?
Case 3: Supply “Echo” function with any data
Case 4
$text = $_GET['text'];
eval("echo('$text');");
What will happen now ?
Case 4: Supply “eval/system” function with “echo command + user data”

หลังจากหลายคนดูโค้ดตัวอย่างไปทั้ง 4 cases ข้างบนแล้ว จะเห็นว่า case 1 และ 4 นั้นสามารถถูกโจมตีประเภท injection ได้. ในขณะที่ case 2 และ 3 นั้นไม่มีทางถูก inject ได้เลย เพราะอะไรนะ…

บางคนอาจจะคิดได้แล้ว

สาเหตุของ OWASP A1 — Injection เกิดจาก ไม่มีการแบ่งระหว่าง command และ user data ออกจากกัน. การนำ command และ user data มารวมกับเป็น String ก้อนเดียว แล้วส่งไปอีก function/service/interpret/etc. ทำให้ user data ที่ไม่ปลอดภัยสามารถล้ำเกินเข้ามาอยู่ในส่วนของ command จน attacker นั้นเข้ามาควบคุม flow control ของการ execute program หรือการ query ได้ เกิดเป็นช่องโหว่ injection นั่นเอง.

จะเห็นว่า case 3 นั้นถูกโจมตีไม่ได้ เพราะใน PHP (ที่จริงก็ทุกภาษา) function กับ parameter นั้นถูกแบ่งออกจากกันอย่างชัดเจนอยู่แล้ว. ไม่ว่าจะใส่ค่าอะไรลง $text ก็ไม่มีทางทำให้มัน echo สองรอบ หรือสั่งคำสั่งของเราได้เลย เพราะ parameter มันอยู่ใน memory ส่วนของ data นั่นเอง. แต่ถ้าเราไปเขียนแบบ case 4 ก็จะทำให้ถูกโจมตีได้ทันที เพราะว่า eval นั้นรับ String ของชุดคำสั่งเข้าไป ไม่ได้มีการแบ่งระหว่าง command และ data ที่ชัดเจน ทำให้หน้าที่การ escape ข้อมูลไปตกอยู่กับ developer ที่เรียกใช้. ไม่ว่าภาษาไหนก็ตาม (น่าจะทุกภาษานะ) จะเห็นได้ว่า มีการเตือนไว้เสมอว่า eval นั้นเป็นคำสั่งที่อันตรายมาก นั่นก็เพราะโดยธรรมชาติของมัน เป็นแบบนี้นี่เองครับ.

ดังนั้น Tutorial SQL ส่วนใหญ่ จึงแนะนำว่าให้ใช้ Prepared Statement วิธีที่ดีที่สุดในการป้องกัน SQL injection !! แล้วไม่ต้อง escape อะไรอีก นั่นก็เพราะ data กับ command มันอยู่คนละ layer/protocol/channel กัน มันเลยไม่ถูกโจมตีแน่ๆโดยที่ไม่ต้อง escape เลย.

แต่… ถ้าใช้ Prepared Statement แต่ไม่ถูกต้อง คือมีการใส่ user data เข้าไปใน Prepared statement เหมือนเดิม ก็แปลว่าเราเอา user data กับ command มาปนกันเหมือนเดิม ก็ถูกโจมตีได้เหมือนเดิม เช่น

$query = "SELECT first_name, last_name FROM users WHERE user_id = (:id) ORDER BY " . $_GET['order'] . "ASC ;"
$data = $db->prepare( $query );

อันนี้เป็นข้อผิดพลาดที่ผมเจอบ่อย เพราะว่า ORDER BY นั้น ไม่ใช่ data แต่เป็น query command. พวก database จึงไม่มี feature Prepared Statement ให้ตรงนี้ (และอื่นๆเช่น ชื่อ Table, column, etc). Developer หลายคนจึงใช้วิธีต่อ String เหมือนเดิม ทำให้ถูกโจมตีเหมือนเดิม.

กรณีนี้ ต้องเริ่มที่ทำความเข้าใจใหม่. ORDER BY คือ ส่วนของ command ที่ไม่ควรให้ user เข้าถึงอยู่แล้ว. ต่อให้ไม่โดน injection แต่ก็คงไม่มีใครอยากให้ ORDER BY Password อยู่ดีหรือปล่าว (อันนี้น่าจะเป็นตัวอย่างที่เห็นชัดเจน). ดังนั้นต้องแก้ด้วยการทำ map user input กับ list ของ command หรือ column ที่อนุญาตให้ ORDER อีกทีครับ.

วิธีการป้องกัน

วิธีการป้องกันมีได้หลายแบบครับ แต่ทั้งนี้ทั้งนั้น คนที่เขียน framework มีส่วนที่ทำให้การป้องกันนั้นง่ายยากซับซ้อนแตกต่างกันด้วย. ซึ่ง developer ที่เรียกใช้ framework, database, library, function นั้นก็จะต้องดูเป็นกรณีไป ว่าคนที่ develop สิ่งนั้นขึ้นมาเนี่ย ต้องการให้เราแบ่งอาณาเขตระหว่าง command และ user data ด้วยวิธีใด.

1. ไม่รับ user input เข้ามาในส่วนของ command, query. เรียกใช้เฉพาะ command, query ที่ hardcoded มาในโปรแกรมเท่านั้น — อันนี้อาจจะฟังดูโง่ แต่มันคือสุดยอดของวิธีการป้องกันเลย. การใช้เฉพาะสิ่งที่เราเขียนและไม่รับข้อมูลภายนอก ทำให้เรามั่นใจว่า ไม่มีอะไร inject เข้ามาเปลี่ยน flow โปรแกรมของเราได้. ถ้าเห็นตัวอย่างข้างล่าง น่าจะอ๋อเลย มันเป็นสิ่งที่บางคนก็ทำเป็นประจำอยู่แล้ว. การ mapping นั่นเอง.

Case 1:
$query = "SELECT first_name, last_name FROM users ORDER BY " . $_GET['order'] . "ASC ;"
Case 2:
if (mode == 1) {
$query = "SELECT first_name, last_name FROM users ORDER BY firstname ASC;"
} else if (mode == 2) {
$query = "SELECT first_name, last_name FROM users ORDER BY lastname ASC;"
}

2. มีการแบ่งโดยธรรมชาติอยู่แล้ว (ถ้าลงลึกหน่อย มันแบ่งออกจากกันด้วย memory address คนละตำแหน่งกันครับ) ได้แก่ program instruction (function) กับ parameter เช่น สั่ง echo($user_data) ยังไงก็ไม่มีทางโดน inject ได้. ดังนั้น default คือปลอดภัย แล้วให้ไปสนใจ function ที่อันตรายแทน. ซึ่ง function ที่อันตราย มันจะรับ data ทั้งก้อนเข้าไป interpret เป็น command โดยไม่ได้แบ่ง command กับ user data ออกจากกันละ.

function อันตราย มีทั้งที่รู้จักกันชัดเจน เช่น eval, system, shell_exec. มีทั้งที่รู้บ้างไม่รู้บ้าง เช่น serialize/de-serialize และมีที่แอบซ่อนมา เช่น assert (PHP), input (Python2), open (Perl) เป็นต้น วิธีที่จะรู้ได้ คือ ให้อ่าน document ครับ. มันมักจะมีเตือนแดงๆบอกไว้. หรือถ้า stackoverflow ก็ต้องลองไล่อ่าน comment ดูด้วย อย่าอ่านแค่คำตอบครับ. การหลีกเลี่ยงการใช้คำสั่งเหล่านี้โดยไม่จำเป็น ทำให้ลดความเป็นไปได้ที่จะเจอ Critical issue บนโปรแกรมของเราโดยที่ไม่รู้ตัวเลยนะครับ. (pentester แบบผมแอบเซงนิดๆ 555)

https://docs.python.org/2/library/functions.html#input คำเตือนใน python2. ดันไม่มีตัวแดง -..-

อย่างในกรณีภาษา c นั้นก็มี function system และ execve ครับ ซึ่งหลายที่ก็จะแนะนำให้ใช้ execve มากกว่า system. เพราะกรณีนี้แหละครับ https://wiki.sei.cmu.edu/confluence/pages/viewpage.action?pageId=87152177

ส่วนในกรณีที่ใช้ serialize/de-serialize อยู่ ผู้อ่านอาจจะอยากกลับไปคิดทบทวนดูว่ามีความจำเป็นต้องใช้เพียงใด และมองวิธีที่ปลอดภัยกว่า เช่น ใช้ JSON แทน เป็นต้น

3. มี channel ในการแบ่งระหว่าง command และ data ให้เลย. ไอเดียคล้ายๆข้อ 2 ครับ เช่น SQL prepared statement / PDO โดยถ้าใช้งานได้ถูกต้อง ก็ไม่จำเป็นต้อง escape user data. อันนี้ผมมองว่าเป็น optional feature สำหรับแต่ละ service นะครับ. database บางตัวอาจจะไม่มีก็ได้. ถ้ามีก็ถือว่า service นั้นเขียนมารองรับผู้ใช้ได้ดีทีเดียว. เมื่อใช้แล้วไม่ต้อง escape ก็จริง แต่ก็ต้องใช้ด้วยความเข้าใจอย่างถูกต้องด้วยครับ ว่าอะไรคือ command/query ก็ห้ามเอา user data เข้าไปอยู่ในนั้นอีก.

4. เลือก alternative way ในการเรียกใช้ function บางอย่างในการทำงาน ได้แก่ การเปลี่ยนจากการเรียก shell execute มาเรียกใช้ function/library ที่ภาษาตัวเองมี เช่น

Case 1
$target = $_GET['target'];
$cmd = shell_exec('curl ' . $target);
What if $ip = https://google.com; rm -rf /home/ ?
Using shell execution without input validation / escape

อย่างด้านบนนี้มีการเรียกไปยัง shell ให้ทำคำสั่ง curl เพื่อเช็คว่าเว็บนั้นเปิดอยู่หรือไม่ ซึ่งมันมีวิธีทำแบบเดียวกัน แต่อยู่ภายใน function/library ของ PHP เอง คือ

Case 2
$target= $_GET['target'];
$ch = curl_init();
curl_setopt($ch, CURLOPT_URL, $target);
curl_setopt($ch, CURLOPT_HEADER, TRUE);
curl_setopt($ch, CURLOPT_NOBODY, TRUE);
curl_setopt($ch, CURLOPT_RETURNTRANSFER, TRUE);
$head = curl_exec($ch);
$httpCode = curl_getinfo($ch, CURLINFO_HTTP_CODE);
curl_close($ch);
Is it possible to inject something into $target like Case 1 ?
Using Curl from PHP library

จะเห็นว่าพอเราเลือกใช้วิธีนี้ ทำให้มันไปตกอยู่ในการป้องกันกรณีที่ 2 คือ function กับ parameter มีการแบ่งชัดเจนอยู่แล้ว ไม่สามารถถูก inject ได้ โดยที่เราไม่ต้องมานั่ง escape นั่นเองครับ (แต่อย่าลืม input validation เน้อ ไม่งั้นจะโดนโจมตีแบบอื่นที่ไม่ใช่ injection)

5. ใช้ escape character. ซึ่งจริงๆแล้วหน้าที่ของการ escape ก็คือการแยกระหว่าง data กับ command นี่แหละครับ เช่น มีการใช้ backslash คู่กับ double quotes เพื่อบอกว่า double quotes ข้างหลังนี้ ให้มองเป็น data นะ. มีใช้เช่นใน SQL query, JSON เป็นต้น.

JSON structure (blackslash is escape character)

การ escape นี่อาจจะมองว่าดูง่าย แต่ต้องระวังทั้งคน design framework และ developer เองครับ ถ้าในกรณีที่อาจจะมี nested double quotes, quotes เยอะๆแล้วมี backslash ติดมามากๆ โอกาสผิดค่อนข้างสูงทีเดียว. เมื่อเถียบกับการแบ่ง channel ซึ่งปลอดภัยกว่าแล้ว หากมี feature แบบข้อ 3 หรือ 4 ให้ใช้ ก็เรียกใช้เถอะครับ.

วิธีการ escape นี้ก็เป็นไปตาม design ของ framework เองว่าจะให้ escape แบบใด. ผมสังเกตว่าส่วนใหญ่จะมี escape ไว้เสมอ น่าจะเพราะ implement ง่ายกว่าการแบ่ง channel แบบ prepared statement. แต่ต้องระวังพอสมควร เพราะหาก design อันนี้ผิดพลาด คือหายนะของ framework นั้นเลยนะครับ. ซึ่งในอดีตก็เคยมีมาแล้ว ทำให้ developer แทบจะไม่สามารถป้องกันตัวเองได้เลย. อย่างเคสในลิงค์นี้ถือเป็นเคสที่น่าสนใจที่ผมจำได้แม่นเลยทีเดียว https://gist.github.com/Zenexer/40d02da5e07f151adeaeeaa11af9ab36

ต้องระวังว่าแต่ละ service, framework อาจจะเลือก escape character ไม่เหมือนกัน เช่น escape character ของ single quote ใน sqlite จะเป็น single quote นำหน้าหนึ่งตัว. ไม่ใช่ backslash นะครับ

A string constant is formed by enclosing the string in single quotes (‘). A single quote within the string can be encoded by putting two single quotes in a row — as in Pascal. C-style escapes using the backslash character are not supported because they are not standard SQL. https://www.sqlite.org/lang_expr.html

ดังนั้น query ที่มี value ประกอบด้วย single quote ใน sqlite จะเป็นแบบนี้ครับ

INSERT INTO table_name (field1) VALUES ('This''s my blog');

หากดันไปใช้ blackslash เพราะความเคยชิน ก็โดน injection แบบมึนๆได้ แนะนำให้ตามอ่านจาก document หรือ stackoverflow เพื่อเช็คความถูกต้องครับ

6. Input validation. เป็นข้อหนึ่งที่อาจจะดูธรรมดาแต่ทรงพลังในการป้องกันมาก แต่ขณะเดียวกัน ก็อาจจะลดความทรงพลังใน feature หรือลูกเล่นต่างๆของ application ไปได้มากทีเดียว. การใช้งานควรดูความเหมาะสมตาม business requirement ครับ. การ validation มีสองประเภท คือ

  • Whitelist คือการกำหนดว่าอนุญาต input ประเภทใดบ้าง จินตนาการว่าทั้งจักรวาลเราอนุญาตแค่นี้ซึ่งเรามั่นใจได้ว่าปลอดภัย มันจึงเป็นวิธีที่ดีที่สุด. เหมาะกับข้อมูลที่ทราบได้ง่าย มี pattern เช่น citizen ID, postal code, telephone number, real name (คงไม่มีใครใส่ double quotes หรือ newline เข้ามา)
  • Blacklist อันนี้คือการ block การ process ของข้อมูลบางประเภทหรือบาง pattern. มีข้อเสียคือ เราจำเป็นต้องรู้ว่ามีข้อมูลประเภทไหนที่อันตรายบ้างจากทั้งจักรวาลแล้ว list ออกมาให้หมด. การหลุด 1 กรณี คือ 1 กรณีที่ทำให้ application ถูกโจมตีได้สำเร็จ. แต่ละภาษาหรือ Service ก็จะมี blacklist ที่ไม่เหมือนกัน ต้องศึกษาดีๆครับ. ดังนั้นจึงไม่แนะนำวิธีนี้

Blacklist และ Whitelist จะช่วยป้องกัน Injection ได้ เมื่อเรากรองอักขระที่เป็น syntax ของ command ออกไป อันนี้แตกต่างไปตามภาษา เช่น double quotes, semi-colons, and/or signs, etc. ดังนั้นใน requirement กับข้อมูลบางกรณีที่ต้องใช้อักขระพวกนี้อยู่ จะมีปัญหากับวิธีนี้แน่นอน. ให้ลองดูวิธีอื่นๆแทน.

7. และอื่นๆ ? อาจจะเป็นวิธีที่ผมลืมพูดถึง พลาด หรือไม่ทราบก็ได้ แต่จากที่กล่าวมาข้างบน ผู้อ่านเข้าใจแล้วว่าสาเหตุนั้นเกิดจากอะไร ดังนั้นถ้าจะเกิดวิธีป้องกันอันใหม่ขึ้นมา เพื่อป้องกันการซ้อนทับกันระหว่าง user data กับ command ก็ไม่แปลกครับ.

การถูกโจมตี injection ประเภทต่างๆ

มาลองดู injection ประเภทต่างๆกันนะครับ จะเห็นว่าถึงแม้การทำงานจะต่างกัน การป้องกันจะต่างกัน แต่ root cause ที่ทำให้ถูกโจมตีนั้นเหมือนกันเลย ถ้าเราไล่ดูดีๆว่าสำหรับ service/framework/library นั้น อะไรคือข้อมูลประเภท command/flow control/query และอะไรคือข้อมูลทั่วไป แล้วพยายามแยกมันออกจากกันให้ได้ ก็จะสามารถป้องกันการโจมตี injection ต่างๆได้หมดเลยครับ

  • Command Injection — การถูก inject ผ่านคำสั่งประเภท system / shell_exec / shell execution ต่างๆ. ทำงานภายใต้กรอบของ shell.
  • Code Injection — ถูก inject ผ่านคำสั่งประเภท eval, serialize ต่างๆ. แตกต่างจาก command injection ตรงที่มันทำงานภายใต้ภาษานั้นๆ.
  • SQL Injection — ถูก inject โดยการใส่ query syntax ปนเข้ามาใน user data
  • LDAP injection — คล้าย SQL แต่คนละ syntax กัน. ถ้ามองกว้างขึ้น บางคนเรียกว่า NoSQL injection
  • XPATH Injection — เป็นภาษาประเภท query แต่เนื่องจาก syntax ที่แตกต่างกับ SQL พอสมควร ทำให้หลายคนอาจจะมองไม่ชัด. สำหรับ XPATH นั้น slash ถือว่าอยู่ในส่วนของ query command ที่ไม่ควรให้ user เข้าถึง. ถ้าให้เปรียบเทียบ (ไม่ถูกต้องเป๊ะๆ แต่น่าจะเห็นภาพ) slash มันคือการเลือก column/table น่ะครับ ซึ่งไม่น่าจะให้ user เลือกเองอยู่แล้ว.
  • Server-Side Template Injection อันนี้น่าสนใจมาก และน่าจะเป็นตัวอย่างที่เกิดจากความไม่เข้าใจระหว่าง data และ command ของหลายๆคน. บวกกับ template จำนวนมากขึ้นในปัจจุบัน ทำให้พบช่องโหว่ประเภทมากขึ้น
  • Cross-site Scripting (XSS) — ถึงมันจะเป็นหัวข้อแยกหนึงใน OWASP แต่มันก็คือ injection ดีๆนี่เองครับ. โยน user data เข้าไป กระพริบตาอีกที กลายเป็น Javascript command เรียบโร้ย.

มีอีกหลายตัวที่น่าสนใจ และผู้อ่านสามารถลองไปอ่านเองได้ (แต่ก็เหตุผลเหมือนเดิม) เช่น Server-Side Includes (SSI) Injection, PHP File inclusion (include/require), Format string attack, JSON injection เป็นต้น

เอาความรู้ไปใช้ประโยชน์ต่อยังไงดี

สำหรับ Developer ทั่วไป

น่าจะมีความเข้าใจมากขึ้น ถึงสาเหตุที่เกิด injection ขึ้นมา ต่อไปก็จะได้ระวังตัวมากขึ้น หรือแม้แต่เอะใจมากขึ้นในขณะที่เขียนอยู่ ว่า เห้ยเรากำลังรวม command กับ user input เข้าด้วยกันนี่หว่า เป็นต้น. เน้นไปที่ความเข้าใจในสิ่งที่กำลังทำอยู่ อย่า copy paste หรือเล่นท่าง่ายอย่างเดียว ลองดูท่าอื่นๆที่อาจจะเขียนยากกว่ายาวกว่า แต่มีความถูกต้องปลอดภัยมากกว่าด้วย. หากไม่แน่ใจ document ช่วยท่านได้.

สำหรับ Framework Developer

มีความเข้าใจมากขึ้นในสิ่งที่ตนกำลังออกแบบ. มีความเข้าใจในการจัดการกับ user input ที่ถูกต้อง โดยเข้าใจความแตกต่างของการทำ escape, prepared statement หรือการเขียนเป็น function ที่รับ parameter เป็น pure data เท่านั้น. ความเข้าใจเหล่านี้จะทำให้ framework ออกมาถูกต้อง และเป็นมิตรกับ developer ด้วยครับ.

วิธีผมค่อนข้างแปลกสินะครับ ปกติหลายคนจะเน้นว่า ห้ามอักขระนู้นใน service นี้ ห้ามอันนี้ในตัวนู้น แต่ผมชอบพูดเป็น general case ภาพรวมมากกว่า แล้วให้ไปคิดต่อยอดกันเองครับ (ซึ่งถ้าเป็นไปได้ การเลือกวิธีป้องกันวิธีอื่น ก็จะดีกว่าการห้ามอักขระนู้นนี้ ซึ่งเป็นการ Blacklist ครับ อาจจะหลุดได้) ทั้งหมดนี้ ไม่ได้บอกว่ามุมมองของคนอื่นผิด ผมอยากเสนอมุมมองของผมที่คิดว่าทำให้ผู้อ่านสามารถเข้าใจพื้นฐานและไปศึกษาต่อใน service ที่ตัวเองสนใจต่อไปได้ง่าย มีอะไรผิดถูกตรงไหน หรืออย่างให้อธิบายเพิ่มตรงไหน ทักมาได้นะครับ

- July 17, 2018 -

--

--