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 $ 인 값을 찾으면 된다.
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를 다운받을 수 있다.
대회기간은 9.11 am 9 ~ 9.12 am 9 으로 24h동안 진행되었는데 다음날 아침이 되보니 조금 아쉬웠습니다.
리버싱, 크립토, 웹을 톡톡 건들여 보다가 젤 방향이 보이는 웹에 몰두해서 다행히 한 문제를 풀어냈습니다.
제가 푼 웹 문제는 사실 OSINT에 가까웠습니다.
mini-realworld란 문제를 풀었는데 ( 이름부터 osint처럼 생기긴 했습니다. )
1. jwt none algorithm을 이용해서 admin token을 만들어냅니다.
전 jwt.io를 활용하는데 인터페이스가 편해서 좋습니다.
2. mypage에서 다양한 정보를 얻을 수 있는데 "Whitehat-NowYouSeeMe"와 "commit ~~~"가 중요 정보라고 볼 수 있습니다.
3. 바로 github를 찾아보니 repo가 존재해서 commit 기록을 살피면서 jenkins 접속 주소와 api token 정보를 얻을 수 있었습니다.
4. 사실 여기서 1차 해맨게 전 당연히 admin api token인 줄 알고 admin으로만 하다 계속 실패했습니다.
5. Whitehat-NowYouSeeMe 계정으로 cli를 통해 접근하면 됩니다.
curl ${JENKINS_주소} --user 계정:API_TOKEN
6. 이후 4시간동안 무한한 삽질을 시작합니다. createUser 부분을 찾아내서 post로 form data보내서 계정 만들어서 접속까지 했습니다.
7. 그래서 스크립트 콘솔로 쉘을 실행하는데 딱 ls 바이너리만 실행권한이 있길래 쉘에서 뭘 하는게 아닌가 하고 열심히 뒤졌는데 누군가가 제 계정 비번을 계속 바꿉니다.
8. 그래서 아 귀찮네 하고 다시 cli에서 /scriptText를 통해서 스크립트 결과를 받는데 그냥 혹시나 해서 ls /tmp 했는데
FLAG가 왜 여기서 나오죠...?
흑 재밌었는데 너무 삽질한 나머지 현타오는 문제였습니다.
제가 건들였던 것은 일단 BitTrader란 문제가 있었는데
sqli가 되는데 from 절 이후로 파라미터에 입력하는 거 같은데 여러 문자가 필터링 됩니다.
필터링 대상 : single quote, double quote, backtick, parentheses <-- (, )
정보 조회로 제일 먼저 @@version 정보를 알아내니 5.7.35 여서 왜 5.7버젼일 까를 고민하다 생각한게 아 파일에 뭔가 하는 거다라는 생각이였습니다.
그래서 @@Global.secure_file_priv도 조회하니 /tmp더라고요.
그래서 아 /tmp 에 있는 세션을 읽거나 아니면 세션에 내가 원하는 값을 넣는거다
근데 문제가 load_file을 하기엔 parenthesis가 필요하고 into outfile을 하기엔 quote가 필요합니다.
여기서 접고 real world 갔습니다.
후반부엔 join을 써먹어볼 까 했는데 join을 어떻게 쓰는지 잘 몰라서 결국 시간이 끝났습니다.
join을 좀 더 공부해서 다음에 포스팅 해보겠습니다.
출제자가 공개한 풀이 방향은 join으로 well known table들을 on, where 절을 통해 불러오는 heavy query 였습니다.
select * from ${symbol} 일지 select col_name, col_name from ${symbol}일지로 고민했는데
select * from price_$[symbol} 이였을 줄이야 ㄴㅇㄱ...
mudbox는 선배한테 풀이를 조금 들었는데 openbase dir bypass 이외에도 disable_funtions 를 우회해서 풀 수 있다고 한다 그걸 좀 더 알아봐야 겠다.
진짜 보면 볼 수록 PHP란 언어는 CTF 특화인가 싶네,,,
N = 2048bit prime ( not p * q )
e = 0x10001
flag_enc = flag^e mod N
ed mod phi(N) = 1 인 d로 decrypt 가능
output이 enlighten 함수 실행결과
hex ( random(1~2^1024) * 2048bit prime + (1~2^1022*3) )
0A가 일종의 separator인듯
0A로 output을 구분 가능할 것으로 보임
euclid 문제는 여기까지 정리하고 음 random의 특수 경우의수를 기대하는 건 아닐거 같고 codegate babyrsa때 처럼 bit로 장난치는건가 했는데 나중에 주워들으니 Approximate Common Division이라는게 있는 것 같다.
100%... 200% ... 300% 까지 심각해지고 있어서 docker log를 확인하였습니다.
find . -name "flag"를 많은 유저분께서 돌리고 계셨습니다.
SSTI 취약점이 별다른 우회없이, 막힌 것 없이 돌아가서 더더욱 초반부터 의도하지 않았을 DoS 공격이 이루어졌습니다.
컨테이너의 상황을 알기 위해서 사용한 명령어들은 아래와 같습니다.
# docker container 전체 상황 확인
# --no-stream은 딱 그 순간만 보여주는 것 기본은 명령어를 치고나서 계속 보여준다.
docker stats --no-stream
# 특정 컨테이너 상황을 보는법
docker stats ${container_name} --no-stream
# 컨테이너 로그 확인
docker logs ${container_name}
# 컨테이너 로그 위치
# 전 해당 경로에 접근하기 위해서 root 권한이 요구됬었습니다.
/var/lib/docker/cotainers/${CONTAINER_ID}/${CONTAINER_ID}-json.log
그래서 좀 cpu 사용률을 제한 할 수 없나 하고 사용한 것이 바로 --cpus 옵션입니다.