티스토리 뷰
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 값이 노출되는 것을 확인할 수 있습니다.
- 끝 -
'보안 > CTF' 카테고리의 다른 글
[HackTheBoo] [Web] Cursed Secret Party Writeup(문제풀이) (0) | 2022.10.28 |
---|---|
[HackTheBoo] [Web] Juggling Facts Writeup(문제풀이) (0) | 2022.10.28 |
[HackTheBoo] [Web] Spookifier Writeup(문제풀이) (0) | 2022.10.28 |
[HackTheBoo] [Web] Evaluation Deck Writeup(문제풀이) (0) | 2022.10.28 |
[ASIS CTF] Beginner ducks Writeup (0) | 2022.10.25 |