저번에 우리는 문제를 출제하기 위한 소재로 취약점을 정하고 분석해보는 시간을 가졌다.
이제 문제를 출제하기 위한 틀을 만들고 취약점을 터트릴 부분을 찾아보자.

코드는 별거 없다. 기본적인 CRUD 에 회원가입, 로그인, 로그아웃 정도가 끝이다.

소스는 아래 링크에 남겨두었다.

https://github.com/nogie-dev/CTF-ETC/tree/main/%EB%82%B4%EB%B6%80-%EB%A9%B4%EC%A0%91%EC%9A%A9-Web%EB%AC%B8%EC%A0%9C


🚩 시작

문제의 메인 페이지를 살펴보면



//main.hbs
{{#if isLogin}}
    <h1>hello {{username}}</h1>
    {{#if isAdmin}}
        <h1>You are Admin</h1>
          <h1>flag{haha_this_is_flag}
    {{else}}
        <h1>You are not admin</h1>
    {{/if}}
    <a href="/users/logout">logout</a>
{{else}}
<h1>not logined</h1>
<a href="/users/login">login</a>
<a href="/users/login">register</a>
{{/if}}

 

만약 로그인이 되지 않았을 경우에는 not logined 라는 문자열을 띄우고

 

 

로그인을 성공했을 경우에는 사용자의 ID를 출력해주고 사용자가 admin 인지 아닌지를 알려주는 메세지가 출력된다. 만약 ID가 admin일 경우 Flag가 출력되는데 이 Flag를 통해서 점수를 획득할 수 있다.

 

//index.js
var express = require('express');
const query=require('../db/mysql_query')
const app = require('../app');
var router = express.Router();

router.get('/',async(req,res)=>{
    console.log(req.session.user)
    if(!req.session.user){
        res.render('main',{isLogin:false})
    }else{
        res.render('main',{
            isLogin:true,
            isAdmin:await query.adminCheck(req.session.user.id).then(res=>res),
            username:req.session.user.id
        })
    }
})

//============================================================
//mysql_query.js 
//adminCheck Function

adminCheck:async function(id){
        try{
            let [rows,field]=await conn.then((connection)=>connection.query("select is_admin from users where id=?",[id]));
            console.log(rows)
            return rows
        }catch(error){
            console.log(error)
            return {"status":"400","msg":"bad request"}
        }
    },

module.exports = router;

 

이 코드는 index.js로 main.hbs를 담당하는 코드이다.

( 사실 express에서 async를 저런식으로 사용하면 Error Handling 문제로 위험하다는 것을 알고 있지만 빠른 제작을 위해서 패스했다.. )

 

정상적인 방법으로 Flag를 획득하기 위해서는 위의 조건에서 isAdmin 으로 받아오는 값이 이어야 하지만 이는 admin 계정으로 로그인을 하지 않는 이상 불가능하다. admin 계정은 소스 코드상으로 추가할 수 있는 로직이 존재하지 않고 mysql console 에서만 추가할 수 있도록 처리했다.

 

남은 방법은 admin을 알아내 직접 로그인을 하는 방법 뿐인데 우리가 저번에 배운 취약점을 써먹을 때가 왔다.

 

우린 이 취약점으로 Local File Read(LFR)을 할 수 있다는 사실을 알고있다.

 

그래서 처음에 DB를 Setting할 때 admin 계정을 추가하는 식으로 작업을 진행했다.
참가자들이 이 문제를 Local 에서 테스트할 수 있도록 환경을 미리 준비해뒀다.

 

//query.sql
create table board(
no int unsigned not null primary key auto_increment,
name varchar(10) not null,
title varchar(30) not null,
context varchar(500) not null
);

create table users(
user_no int unsigned not null primary key auto_increment,
nickname varchar(10) not null,
id varchar(20) not null,
password varchar(20) not null,
is_admin boolean not null default 0 
);

//insert admin account
insert into users(nickname,id,password,is_admin) value("admin","##########Redacted#########","##########Redacted#########",1);

 

admin 계정의 id와 pw는 블라인드 처리하고 DB Setting 파일을 올려두었다.

이제 사용자가 저 init_query.sql 쿼리 파일을 Leak 하면 admin 계정을 탈취하여 로그인하면 Flag를 얻을 수 있을것이다.


⚠ 취약점 발생

 

문제 풀이 방향을 정했으니 '어디에', '어떻게' CVE-2021-32820 으로 LFR을 터뜨릴지 찾아봐야 한다.
로그인과 회원가입에서 터뜨리려먼 SQL Injection 을 이용해야 하니 PASS.

제일 만만한곳이 Board 부분이다.

 

var express = require('express');
const query=require('../db/mysql_query')
var router = express.Router();

...
router.post('/preview',(req,res)=>{
    if(!req.session.user){
        res.json({"status":"401","msg":"Unauthorized"})
    }else{
        let content=req.body
        //console.log(tmp)
        res.render('board/board_preview',content)
    }
})

router.get('/write',(req,res)=>{
    if(!req.session.user){
        res.json({"status":"401","msg":"Unauthorized"})
    }else{
        res.render('board/board_write',{username:req.session.user.id})
    }
})

router.post('/write',(req,res)=>{
    if(!req.session.user){
        res.json({"status":"401","msg":"Unauthorized"})
    }else{
        const {name,title,context}=req.body

        if(title!=''&&context!=''){
            query.createBoard(name,title,context)
            .then((queryRes)=>{
                //res.json(queryRes)
                res.redirect('/board')
            })
        }else{
            res.render('board/board_write',{empty:true})
        }
    }
})
...

module.exports = router;

 

필요한 부분을 제외하고 모조리 생략했다.

저번에 봤던 취약점을 떠올려보자.

 

객체에 Property로 layout이 들어갈 경우 layout의 값으로 들어간 경로의 파일을 렌더링 해주는 것을 확인 했었는데 위의 write와 preview에서 render의 options 인자를 보면 preview는 req.body를 통째로, write는 username이라는 Property에 값을 설정하고 render 해주는걸 확인할 수 있는데 분석한대로 라면 preview에서 취약점이 터지는 것을 알 수 있다.

 

다음은 board_write.hbs의 내용이다.

 

//board_write.hbs

<!DOCTYPE html>
<html lang="en">
    <head>
        <meta charset="UTF-8">
        <meta name="viewport" content="width=device-width, initial-scale=1.0">
        <title>Login Form Design</title>
        <link rel="stylesheet" href="https://pro.fontawesome.com/releases/v5.10.0/css/all.css">
        <link rel="stylesheet" href="../../static/stylesheets/style.css">
    </head>
    {{#if empty}}
        <script>alert('title or context is empty')</script>
    {{/if}}
    <body>
        <form method="POST" class="board-form">
            <h1>Write</h1>

            <h3>writer : {{{username}}}</h3>

            <input type="hidden" name="name" value={{{username}}}>

            <div class="textb">
                <input type="text" placeholder="title" name="title">
            </div>

            <div class="text_board">
                <input type="text" placeholder="context" name="context">
            </div>

            <input type="submit" class="btn-board" value='작성'></button>
            <input type="submit" class="btn-board" onclick='return submit2(this.form);' value='미리보기'></button>![](https://velog.velcdn.com/images/nogie/post/50c3a54a-6efa-4fd3-91a6-b9e6927ca244/image.png)

        </form>
    </body>

<script> 
  function submit2(frm) { 
    frm.action='/board/preview'; 
    frm.submit(); 
    return true; 
  } 
</script> 

 

만약 정상적으로 preview에 요청을 보낼 경우 preview의 content 변수에는
{'name':'idk', 'title':'tmp', 'context':'hello world'} 이런식으로 값이 담기고

 

 

 

예상했던대로 처리 되는 모습을 확인할 수 있다.

 

그러나

 

 

이와 같이 input 태그에서 name을 layout으로 변경해서 preview에 요청을 보낼 경우
content 변수에 {'name':'idk', 'title':'tmp', 'layout':'../../init_query/query.sql'} 이렇게 값이 담겨 결국 File Leak이 가능해진다.

 

 

짜잔


📋 후기

사실 문제를 만들기 시작했을 때만 해도 취약점에 대한 이해 없이 무작정 구현하여 빨리 제작하는데에만 집중했다.

이렇게 글을 작성하면서 취약점에 대해 찾아보고 소스들도 분석해보면서 실력이 조금씩 늘어나는 것 같다.

복사했습니다!