본문 바로가기

4-1. 2024-2 심화 스터디/워게임 도장 깨기

[5주차] 2411104 워게임 도장 깨기

Web | old -05

(1) 문제 살펴보기

  • Login, Join버튼이 있음
  • Login창에 admin으로 로그인을 해보니 wrong password라는 문구가 뜬다.
  • Join을 눌러보니 Access_Denied라는 문구가 뜬다.

(2) 소스코드 확인해보기

 

 

  • mem/디렉토리가 있는 것을 확인할 수 있었고 따라서, url에 mem/을 입력

  • mem디렉토리에 join.php, login.php의 파일이 있음
  • 우선 join페이지가 denied가 되어 있었기에 먼저 join.php에 접근해보았다.

 

  • 접근하자마자 bye라는 문구가 뜨고 확인을 눌렀더니 그 전과 같이 메인페이지로 이동하는 것이 아니라 join.php디렉토리 안에 있음
  • 따라서 join.php페이지의 소스코드를 살펴보았음

  • if문 위는 모두 변수 선언이라 제외하고 중요한 if문만 정리해보았다.
  • 위의 사진은 여러 문자들이 작성되어 있어 해석하기 어려운 상황이다. 따라서 콘솔창을 활용해 아래와 같이 해독하여 코드를 다시 작성해보았다.

 

if(eval('document.cookie').indexOf('oldzombie')==-1) {
alert('bye');
throw "stop";
}
if(eval(document.URL).indexOf("mode=1")==-1){
alert('access_denied');
throw "stop";
}else{
document.write('<font size=2 color=white>Join</font><p>');
document.write('.<p>.<p>.<p>.<p>.<p>');
document.write('<form method=post action='+ 'join.php' +'>');
document.write('<table border=1><tr><td><font color=gray>id</font></td><td><input type=text name='+ 'id' +' maxlength=20></td></tr>');
document.write('<tr><td><font color=gray>pass</font></td><td><input type=text name='+ 'pw' +'></td></tr>');
document.write('<tr align=center><td colspan=2><input type=submit></td></tr></form></table>');
}

 

  • 소스코드를 분석해보자.
  • 첫 번째 if문은 쿠키에 oldzombie값이 없으면 ‘bye’를 출력하는 alert창을 띄우는 코드이다.
  • 두 번째 if문은 URL에 mode=1이라는 값이 없으면 ‘access_denied’를 출력하는 alert창을 띄우는 코드이다.
  • 따라서, URL에 mode=1을 넣고 쿠키가 oldzombie가 되면 또 다른 힌트를 얻을 수 있을 것이다.

 

 

  • 그랬더니 위의 사진과 같이 join페이지가 보이는 것을 확인할 수 있음
  • 따라서 임의의 ID와 PW로 가입하고 login페이지에 로그인을 시도하였더니 admin으로 로그인해야한다는 문구가 출력

  • 따라서, 다시 join.php페이지에 들어가 admin, admin으로 로그인을 시도했는데 아이디가 이미 있다는 알림창을 볼 수 있었다.

  • SQL에서 값을 저장할 때 SET id=값 또는 INSERT id=값 형식으로 저장하기 때문에 공백을 활용하여 문자를 우회하여 시도했다. (admin과 (공백)admin은 다른 값임)

(3) SQL 쿼리 우회 방식

  • 논리 연산자 기반 우회
    • OR 또는 AND 연산자를 사용하여 특정 조건을 우회하는 방식
    • SELECT * FROM users WHERE username = 'admin' OR '1'='1';
    • 항상 참이 되어 로그인 같은 인증 절차를 우회
  • 쿼리의 일부를 무효화하여 우회
    • --, #, /* */ 같은 주석 기호를 사용해 쿼리의 일부를 무효화하여 우회
    • SELECT * FROM users WHERE username = 'admin' -- AND password = 'password';
  • 공백 및 문자열 변형
    • users 테이블에 username과 password가 저장되어 있다고 가정하고, username에 admin과 admin (공백이 포함된 admin)을 입력
    • 데이터베이스는 서로 다른 값으로 인식하게 됨
    • INSERT INTO users (username, password) VALUES ('admin', 'password123'); INSERT INTO users (username, password) VALUES (' admin', 'password456');
  • 다시 Login.php에 들어가 admin으로 로그인을 시도했더니 문제가 풀렸다.

Web | dreamhack 1 - amocafe

문제 첫 화면

=> 아모의 최애 메뉴 번호를 입력하면 flag 획득
=> 코드 분석해 메뉴 번호 확인하기

!/usr/bin/env python3
from flask import Flask, request, render_template

app = Flask(__name__)

try:
    FLAG = open("./flag.txt", "r").read()       # flag is here!
except:
    FLAG = "[**FLAG**]"

@app.route('/', methods=['GET', 'POST'])
def index():
    menu_str = ''
    org = FLAG[10:29]
    org = int(org)
    st = ['' for i in range(16)]

    for i in range (0, 16):
        res = (org >> (4 * i)) & 0xf
        if 0 < res < 12:
            if ~res & 0xf == 0x4:
                st[16-i-1] = '_'
            else:
                st[16-i-1] = str(res)
        else:
            st[16-i-1] = format(res, 'x')
    menu_str = menu_str.join(st)

    # POST
    if request.method == "POST":
        input_str =  request.form.get("menu_input", "")
        if input_str == str(org):
            return render_template('index.html', menu=menu_str, flag=FLAG)
        return render_template('index.html', menu=menu_str, flag='try again...')
    # GET
    return render_template('index.html', menu=menu_str)

if __name__ == '__main__':
    app.run(host='0.0.0.0', port=8000)
  • 변수 org는 FLAG 리스트의 인덱스 10부터 28까지의 값을 갖고 있고 이것을 정수형으로 설정
    변수 st 리스트 생성
  • for반복문 -> 16번 반복
  • res = (org >> (4 * i)) & 0xf
    : org 값을 4비트씩 오른쪽으로 비트 시프트
  • (4 * i) 시프트 => 2진수에서 4bit 단위로 잘라냄 => 16진수 한자리 단위로 잘라냄(16진수 한 글자씩)
  • i가 0일 때 org의 하위 4bit
    i가 1일 때 그 다음 4bit 추출
  • & 0xf
    : 1111(2진수) => F(16진수)
0x 는 16진수
f는 15의미
0xf = 15 * 16^0 = 15

이것을 비트 &(AND)연산 하면 각 비트에서 둘 다 1인 자리만 1로 변환하고 나머지는 모두 0으로..

=> & 0xf 연산 하면 하위 4비트(0xf 부분)만 남기고 모두 0으로 만들어버림
(하위 4비트는 0000~1111로 0~15사이)

=> res는 0x0 ~ 0x15 범위 값으로 제한

if 0 < res < 12:
            if ~res & 0xf == 0x4:
                st[16-i-1] = '_'
            else:
                st[16-i-1] = str(res)
        else:
            st[16-i-1] = format(res, 'x')

res가 1~11일 때
if ~res & 0xf == 0x4:
~res의 하위 4bit만 추출한 것이 4일 때이므로
res가 1011 (~res가 0100이니까..)
즉, res==11 _로 변경

나머지 경우일 때는 res를 문자열 변환해 저장

res가 1~11아닐 때 res를 16진수로 변환해 저장

menu_str = menu_str.join(st)
: 이렇게 만들어진 st리스트의 요소들을 하나의 문자열로 합침

    # POST
    if request.method == "POST":
        input_str =  request.form.get("menu_input", "")
        if input_str == str(org):
            return render_template('index.html', menu=menu_str, flag=FLAG)
        return render_template('index.html', menu=menu_str, flag='try again...')
    # GET
    return render_template('index.html', menu=menu_str)

POST 부분
POST 하면 input_str에 menu_input 입력값 가져옴
input_str이 org와 같다면 flag

GET 부분
menu 변수에 menu_str 전달 index.html 보여줌

 

  • 문제로 돌아와서..
    "My favorite menu is 1_c_3_c_0__ff_3e 🥤"
    이라고 문제 화면에 써 있다.
  • 저 코드 방식대로 문제를 풀었을 때 해당 문자열이 나온다는 것을 알 수 있다.
  • 이 문자열은 16진수로 되어있으니 이것을 2진수로 변경 후 int형으로 바꾸면 원래 값을 찾을 수 있을 것이다.
  • <16진수 -> 2진수>
    1 : 0001
    _ : 1011 (아까 위에서 구한 값)
    c : 1100
    3 : 0011
    0 : 0000
    f : 1111
    e : 1110
  • =>
    0001/1011/1100/1011/0011/1011/1100/1011/0000/1011/1011/1111/1111/1011/0011/1110
  • 0001101111001011001110111100101100001011101111111111101100111110
  • 이것을 10진수로 바꾸면
    2002760202557848382이다.
  • 다음과 같은 코드로 얻어냈다.
  • 파이썬에서 2진수를 10진수로 바꿀 때는 int함수를 써서 구할 수 있다. (int는 10진수 형태)

 


Reversing | Dreamhack putty

 

  • 주요 악성 행위를 하는 함수인 sub_40A360 내 코드를 살펴보면, 127.0.0.1로 연결하여 어떠한 값들을 전송하는 것을 확인할 수 있다.

  • Chrome 브라우저의 암호화된 키를 추출하고, 복호화하여 값을 반환한다.

  • 이후 해당 값을 base64 인코딩하여 DH{}로 감싸 전송한다.
  • 아래의 패킷과 같은 형태가 된다.

  • sub_40A460 함수를 호출하는 곳으로 찾아가보면, Login Data를 인자로 전달하는 것을 확인할 수 있다.
  • 버퍼에 할당된 Login Data 값에 대해 Create key 함수로 생성된 암호화 키 값을 xor 연산하여 4096 바이트씩 전송한다.

  • create_key는 특정 값을 바탕으로 난수를 생성한다. Login Data는 SQLite 3 형식을 가지며, 헤더 값을 바탕으로 초기 값을 특정하였다. 초기값 : 0xb800
  • 이후 아래와 같이 create_key 함수를 포팅하여 코드를 작성하였다.
def decrypt_data(encrypted_data, initial_value=0xb800):
    decrypted = bytearray()
    current = initial_value
    
    for b in encrypted_data:
        # 키 생성
        v1 = (0x343FD * current + 0x269EC3) & 0xFFFFFFFF
        key = ((v1 >> 16) & 0x7FFF) & 0xFF
        current = v1
        
        # XOR 복호화
        decrypted.append(b ^ key)
        
    return decrypted

with open('c:/Users/Owner/Documents/dreamhack/hackputty/packet_binary', 'rb') as f:
    encrypted_data = f.read()

decrypted_data = decrypt_data(encrypted_data)

with open('c:/Users/Owner/Documents/dreamhack/hackputty/decrypted.bin', 'wb') as f:
    f.write(decrypted_data)

  • 암호화된 blob 데이터가 존재한다.
  • 해당 데이터를 복호화하기 위해서는 DPAPI와 Chrome autofill에서 저장되는 방식에 대한 이해가 필요하기 때문에 아래 블로그를 바탕으로 공부하였다.

https://ohyicong.medium.com/how-to-hack-chrome-password-with-python-1bedc167be3d

 

How to hack Chrome password with Python

Do you think it is safe to store your password in Chrome? The short answer is “no”. Any perpetrator that has access to your laptop is able…

ohyicong.medium.com

 

import base64
from Crypto.Cipher import AES
import struct
import sqlite3

def decrypt_password(ciphertext, secret_key):
    try:
        # 초기화 벡터(IV) 추출
        initialisation_vector = ciphertext[3:15]
        
        # 암호문 추출 (마지막 16바이트를 제외)
        encrypted_password = ciphertext[15:-16]

        # AES-GCM 복호화
        cipher = AES.new(secret_key, AES.MODE_GCM, initialisation_vector)
        decrypted_pass = cipher.decrypt(encrypted_password)

        # 복호화된 비밀번호를 UTF-8 문자열로 변환
        return decrypted_pass.decode('utf-8')
    except Exception as e:
        print(f"Error during decryption: {str(e)}")
        return ""

base64_encoded_key = "8u4orW2bmcHxpJA2HRTdpem0ksiY8kKInf8umZnsLbA="

decoded_key = base64.b64decode(base64_encoded_key)
print(decoded_key)

def get_db_connection(chrome_path_login_db):
    try:
        # SQLite 데이터베이스 연결
        conn = sqlite3.connect(chrome_path_login_db)
        return conn
    except Exception as e:
        print(f"Error: {str(e)}")
        return None

def main():
    chrome_path_login_db = r"C:/Users/Owner/Documents/dreamhack/hackputty/decrypted.bin"
    
    secret_key = decoded_key
    
    conn = get_db_connection(chrome_path_login_db)
    if secret_key and conn:
        cursor = conn.cursor()
        cursor.execute("SELECT action_url, username_value, password_value FROM logins")
        for login in cursor.fetchall():
            username = login[1]
            ciphertext = login[2]
            decrypted_password = decrypt_password(ciphertext, secret_key)
            print(f"Username: {username}, Password: {decrypted_password}")
        
        cursor.close()
        conn.close()

if __name__ == '__main__':
    main()

Pwnable | pwnable.kr mistake

  • 힌트: 연산자 우선순위
  • ssh 접속 후 디렉토리/파일 확인
  • ssh mistake@pwnable.kr -p2222

 

#include <stdio.h>
#include <fcntl.h>

#define PW_LEN 10
#define XORKEY 1

void xor(char* s, int len){
	int i;
	for(i=0; i<len; i++){
		s[i] ^= XORKEY;
	}
}

int main(int argc, char* argv[]){
	
	int fd;
	if(fd=open("/home/mistake/password",O_RDONLY,0400) < 0){
		printf("can't open password %d\n", fd);
		return 0;
	}

	printf("do not bruteforce...\n");
	sleep(time(0)%20);

	char pw_buf[PW_LEN+1];
	int len;
	if(!(len=read(fd,pw_buf,PW_LEN) > 0)){
		printf("read error\n");
		close(fd);
		return 0;		
	}

	char pw_buf2[PW_LEN+1];
	printf("input password : ");
	scanf("%10s", pw_buf2);

	// xor your input
	xor(pw_buf2, 10);

	if(!strncmp(pw_buf, pw_buf2, PW_LEN)){
		printf("Password OK\n");
		system("/bin/cat flag\n");
	}
	else{
		printf("Wrong Password\n");
	}

	close(fd);
	return 0;
}

 

코드 해체 

	
	int fd;
	if(fd=open("/home/mistake/password",O_RDONLY,0400) < 0){
		printf("can't open password %d\n", fd);
		return 0;
	}
  • open 성공하면 1 반환 → 0보다 반환된 값(1)이 큼 → fd의 0 저장됨
	char pw_buf[PW_LEN+1];
	int len;
	if(!(len=read(fd,pw_buf,PW_LEN) > 0)){
		printf("read error\n");
		close(fd);
		return 0;		
	}
  • fd = 0 (stdin)
  • 사용자로부터 입력을 받아 pw_buf에 저장, read에서 성공한 바이트만큼 반환
  • 값은 0보다 크므로 len 1이 됨 → ! 연산을 통해 false가 되므로 if문 들어가지 않음
	char pw_buf2[PW_LEN+1];
	printf("input password : ");
	scanf("%10s", pw_buf2);
	
	// xor your input
	xor(pw_buf2, 10);

#위에 있는 xor 함수
void xor(char* s, int len){
	int i;
	for(i=0; i<len; i++){
		s[i] ^= XORKEY;
	}
}

	if(!strncmp(pw_buf, pw_buf2, PW_LEN)){
		printf("Password OK\n");
		system("/bin/cat flag\n");
	}
	else{
		printf("Wrong Password\n");
	}
  • 사용자 입력으로 10byte만큼 받아서 pw_buf2에 저장
  • 한 byte씩 1과 비트 xor 연산 → pw_buf와 값이 같은지 비교 0x1
  • 값이 같으면 flag 출력, 값이 다르면 wrong Password 출력
  • ⇒ 사용자로부터 받은 pw_buf와 pw_buf2 xor 0x1의 값이 같으면 flag 출력
  • ⇒ 간단하게 0과 1을 사용하여 flag 값 출력시킴

 

  • Mommy, the operator priority always confuses me :(

Forensics | Dreamhack lolololologfile

Description

Someone deleted the PDF file which has flag!

How can I recover it?

https://dreamhack.io/wargame/challenges/727

 

lolololologfile

Someone deleted the PDF file which has flag! How can I recover it?

dreamhack.io

  • 주어진 문제 파일은 Image.E01 파일이므로, FTK Imager로 열어줍니다.

  • Image.E01 -> HUNJISON -> [root]에 들어가보면 아래와 같이 삭제된 4개의 파일(seg1, seg2, seg3, seg4)을 확인할 수 있는데, 내부 내용은 확인 불가한 것을 알 수 있습니다.
  • 삭제가 되었음을 확인하고, 비할당 영역(unallocated space)에 들어가서 확인을 해보면, 여러 파일들이 있는데 그 중 "02067" 파일이 PDF 파일임을 확인할 수 있고, 마지막 "05082" 파일에 PDF 파일의 끝을 나타내는 "%%EOF"가 있는 것이 확인되는데, 삭제된 PDF 파일은 이 4개의 파일로 추려볼 수 있습니다.
  • 따라서, 해당 파일을 선택해서 export files를 해주면 복구할 수가 있으므로 제 로컬 PC에서 확인을 해줍니다.