티스토리 뷰

728x90
반응형

Introduction

Category : Web

Difficulty : easy

Description : An unknown entity has taken over every screen worldwide and is broadcasting this haunted feed that introduces paranormal activity to random internet-accessible CCTV devices. Could you take down this streaming service?

문제 첫 페이지

Code Analysis

먼저 Flag를 획득하기 위한 방법에 대해서 알아봅니다. Flag는 config.py에 Flask 환경변수로 저장된 것을 볼 수 있습니다.

class Config(object):
    SECRET_KEY = generate(50)
    MYSQL_HOST = 'localhost'
    MYSQL_USER = 'user'
    MYSQL_PASSWORD = 'M@k3l@R!d3s$'
    MYSQL_DB = 'horror_feeds'
    FLAG = open('/flag.txt').read()

그리고 이 FLAG 는 다시 routes.py 에서 /dashboard 경로를 요청 받았을 때 템플릿을 렌더링할 때 사용됩니다.

@web.route('/dashboard')
@is_authenticated
def dashboard():
    current_user = token_verify(session.get('auth'))
    return render_template('dashboard.html', flag=current_app.config['FLAG'], user=current_user.get('username'))

그리고 dashboard.html 템플릿에서는 관리자(admin)일 때만 Flag가 출력되도록 하고 있는 것을 볼 수 있습니다.

{% if user == 'admin' %}
<div class="container-lg mt-5 pt-5">
    <h5 class="m-3 ms-0">Firmware Settings</h5>
    <h6 class="m-4 ms-0 text-grey">Upgrade Firmware</h6>
    <div class="d-flex align-items-center">
            <img src="/static/images/folder.png" height="25px" class="sw-img">
            <span class="fw-bold sw-text">Software Folder</span>
            <input type="text" class="form-control sw-path" value="/opt/horrorfeeds/Firmware/" disabled>
    </div>
    ... 생략 ...
    <tr class="table-active">
        <th>
            <input class="form-check-input fw-cam-radio" type="checkbox" checked disabled>
        </th>
        <td>5</td>
        <td>192.251.68.6</td>
        <td>NV360</td>
        <td>{{flag}}</td>
        <td></td>
        <td></td>
        <td>admin</td>
        <td>80</td>
        <td>21</td>
        <td>23</td>
        <td></td>
      </tr>
    </tbody>
</table>
<div class="d-flex justify-content-end mt-3 mb-3">
    <button class="btn btn-info fw-update-btn me-3">Upgrade Selected</button>
    <button class="btn btn-danger fw-update-btn">Disable Feeds</button>
</div>

</div>

기능이 로그인, 회원가입, 로그아웃, 그리고 대시보드 확인 기능밖에 없으므로 관리자 세션 탈취나 XSS 문제는 아닌 것으로 보여졌습니다.

 

그런고로 관리자 계정을 탈취해야하는 문제로 보여졌는데, 벡터로 SQL Injection이 있는지 확인해봤습니다.

다행히 register 할 때 INSERT 구문 실행 시 Injection 타겟이 보였습니다.

def register(username, password):
    exists = query_db('SELECT * FROM users WHERE username = %s', (username,))
   
    if exists:
        return False
    
    hashed = generate_password_hash(password)

    query_db(f'INSERT INTO users (username, password) VALUES ("{username}", "{hashed}")')
    mysql.connection.commit()

    return True

위 SELECT 시에는 보호가 되는데, 아래 INSERT 할 때 username 부분이 문자열 그대로 별도의 필터링 없이 들어가고 있는 것을 볼 수 있습니다.

 

다만 현재 환경에서는 stacked query injection 은 불가능하기 때문에 큰 따옴표를 탈출해서 관리자 비밀번호를 변경하는 수밖에 없는 것으로 보여집니다.

 

그리고 마침 CREATE TABLE 할 때 username 컬럼이 UNIQUE 옵션이 설정되어 있는 것을 볼 수 있습니다.

CREATE TABLE horror_feeds.users (
    id INTEGER PRIMARY KEY AUTO_INCREMENT,
    username varchar(255) NOT NULL UNIQUE,
    password varchar(255) NOT NULL
);

이 때 만약 아래와 같이 SQL Injection 을 하게 되면

-- INSERT INTO users (username, password) VALUES ("{username}", "{hashed}")
INSERT INTO users (username, password) VALUES ("admin","1") ON DUPLICATE KEY UPDATE password=concat("", "{hashed}")

username 이 admin 이고 password가 1인 계정을 삽입하려고 할 때 admin 이 이미 있는 계정이니깐 duplicate key 오류가 날 것이고, 이 오류가 났을 때 중복된 항목의 password 컬럼의 값을 사용자가 입력 폼에 입력한 password 로 변경하라는 의미가 됩니다.

 

이제 해당 구문을 실제로 사용해서 admin 계정의 password를 마음대로 변경해봅니다.

Exploit

아래와 같이 입력하고 Register 버튼을 눌러보았고, username이 중복되지 않으니 회원가입이 잘 된 것을 볼 수 있습니다.

그리고 이제는 실제로 admin 계정의 비밀번호가 제가 위에서 입력한 qwer1234 로 변경되었는지 로그인을 통해서 확인해봤습니다.

이렇게 제일 하단에 Flag 값이 노출되는 것을 확인할 수 있습니다.

 

- 끝 -

728x90
반응형
댓글