Description

Category: Web

Source: CONFidence CTF 2019 Teaser

Points: 51

Author: Jisoon Park(js00n.park)

Description:

I think I've found something interesting, but I'm not really a PHP expert. Do you think it's exploitable?

https://gameserver.zajebistyc.tf/admin/

Write-up

주어진 사이트로 들어가 보면 login.php와 login.php.bak를 볼 수 있다. .bak 파일은 login.php의 소스 코드를 보여주기 위해 만들어 둔 것 같다. 코드를 살펴보자.

[...]

if (!preg_match('/^{"hash": [0-9A-Z\"]+}$/', $_COOKIE['otadmin'])) {
    echo "COOKIE TAMPERING xD IM A SECURITY EXPERT\n";
    exit();
}

$session_data = json_decode($_COOKIE['otadmin'], true);

if ($session_data === NULL) { echo "COOKIE TAMPERING xD IM A SECURITY EXPERT\n"; exit(); }

if ($session_data['hash'] != strtoupper(MD5($cfg_pass))) {
    echo("I CAN EVEN GIVE YOU A HINT XD \n");

    for ($i = 0; i < strlen(MD5('xDdddddd')); i++) {
        echo(ord(MD5($cfg_pass)[$i]) & 0xC0);
    }

    exit("\n");
}

display_admin();

otadmin이라는 쿠키를 만들어 줘야 하는데, 숫자와 A-Z로 구성된 hash 값을 JSON 형태로 보내야 한다. 우리가 보낸 값이 $cfg_pass 변수의 md5 hash와 동일하면 admin 관련 내용을 보여주도록 되어 있다.

php에서 문자열과 입력값을 비교할 때는 type confusion을 생각해 봐야 한다. 일단, 알파벳으로 시작하는 문자열은 0과 비교했을 때 true이니, hash 값으로 정수 0을 보내보자.

if 문의 조건을 통과하지 못하는 것을 보니 MD5 hash의 결과가 알파벳으로 시작하지는 않는 것 같다. 힌트로 주는 문자열은 0과 64의 조합이다. 이 문자열은 $cfg_pass 변수의 MD5 해쉬 결과(문자열)의 각 자리에 대한 ascii code에 0xC0를 and 연산 한 결과이다. 0xC0는 이진수로 쓰면 11000000b로, and 연산했을 때 64 미만의 수에 대해서는 0이 나온다. MD5 hash 결과는 숫자 또는 A부터 F 까지의 문자로 구성되어 있을 테니, 이 결과가 0이면 해당 자리가 숫자이고 64이면 문자라는 뜻이다.

결국, MD5($cfg_pass)는 숫자|숫자|숫자|문자|문자|문자|숫자|문자|.... 의 형식으로 구성되어 있다는 뜻이다.

이 힌트에서 얻어야 하는 부분은 맨 앞에 연속된 숫자로 구성된 자릿수가 3자리 라는 것이다. php는 문자열과 숫자를 비교할 때, 문자열이 숫자로 시작하면 그 부분만 숫자로 인식하여 비교한다. (예를 들면, "123AAA" == 123 를 true로 판단한다.)

세 자리 숫자에 대한 경우의 수는 1000가지이니, 간단하게 brute-force 해볼 수 있을 것 같다.

아래와 같은 코드를 작성하여 쉽게 찾을 수 있었고, 그 결과 flag를 받았다.

import requests

url = "https://gameserver.zajebistyc.tf/admin/login.php"

for i in range(1000):
  cookies = {"otadmin": '{"hash": %03d}'%i}
  print cookies
  response = requests.get(url, cookies=cookies)
  print response.status_code
  print response.text

Flag : p4{wtf_php_comparisons_how_do_they_work}

'writeups > Web' 카테고리의 다른 글

sbva  (0) 2019.11.26
You Already Know  (0) 2019.11.26
Cookie Monster  (0) 2019.11.25
Stop and Listen  (0) 2019.11.25
ReadingRainbow  (0) 2019.11.25

Description

Category: Reversing

Source: CONFidence CTF 2019 Teaser

Points: 57

Author: Jisoon Park(js00n.park)

Description:

Elementary, my dear Watson.

elementary.tar.gz

Write-up

int __cdecl main(int argc, const char **argv, const char **envp)
{
  char v4; // [rsp+0h] [rbp-90h]
  unsigned __int64 v5; // [rsp+88h] [rbp-8h]

  v5 = __readfsqword(0x28u);
  printf("Password: ", argv, envp);
  __isoc99_scanf("%s", &v4);
  if ( (unsigned int)checkFlag(&v4) )
    printf("Good job!", &v4);
  else
    printf("Wrong!", &v4);
  return 0;
}

Password를 입력 받는데, 특정 조건을 만족하는 Password 자체가 flag인 전형적인 angr 문제이다.

_BOOL8 __fastcall checkFlag(char *a1)
{
  if ( (unsigned int)function0(a1[64] & 1) )
    return 0LL;
  if ( (unsigned int)function1((a1[64] >> 2) & 1) )
    return 0LL;
  if ( (unsigned int)function2((a1[64] >> 5) & 1) )
    return 0LL;
  if ( (unsigned int)function3((a1[64] >> 4) & 1) )
    return 0LL;

[...]

checkFlag() 함수는 function0() 부터 function831() 까지의 함수를 이용해서 Password를 체크한다.

더 볼것도 없이 angr를 이용해서 password를 찾아보자.

import angr
succ_addr = 0x40077f
fail_addr = 0x400792

proj = angr.Project('./elementary', load_options={"auto_load_libs": False})

sm = proj.factory.simulation_manager()
sm.explore(find = (succ_addr,), avoid = (fail_addr,))

if len(sm.found) < 1:
    print "not found!"
else:
    s = sm.found[0].state.posix.dumps(0)
    print "found : " + s

위와 같이 exploit을 작성하여 실행해 보았으나, angr는 path 탐색에 실패했다. 이래저래 다르게 구현해봤는데 안되는걸 보니, 나중에 다른사람들 writeup을 좀 찾아봐야 할것 같다.

각각의 function이 400개가 넘는 instruction으로 구성되어 있는걸 보면 각각의 함수가 좀 복잡해서 그럴 수도 있을 것 같다. 하지만 IDA에서 decompile 해보면 argument를 그대로 리턴하거나 1과 xor하여 리턴하거나 둘 중의 하나이다.

바이너리를 분석하기 어렵게 일부러 꼬아둔 것 같으니, IDApython을 이용해서 checkFlag 함수와 각각의 functionX 함수들의 decompile한 결과를 파일로 저장하였다.

checkFlag() 함수의 구성 자체는 별다른 예외 없이 일률적으로, if문 하나가 functionX 함수 하나를 이용해서 Password의 한 bit를 확인하는 식이다.

python을 이용해서 조건을 만족시기는 문자열을 찾는 코드를 작성하여 flag를 찾을 수 있었다.

Flag : p4{I_really_hope_you_automated_this_somehow_otherwise_it_might_be_a_bit_frustrating_to_do_this_manually}

'writeups > Reversing' 카테고리의 다른 글

Easy Crack Me  (0) 2019.11.26
i can count  (0) 2019.11.26
ELF Crumble  (0) 2019.11.26
Super Secure Vault  (0) 2019.11.26
Feed_me  (0) 2019.11.25

Description

Category: Crypto/Web

Source: DEFCON CTF 2018 Quals.

Points: 104

Author: Jisoon Park(js00n.park)

Description:

There, I've said it…

http://ssat-ps.iptime.org:5101

samples.tgz

Write-up

(*. 문제 환경을 재구성하여 풀이하였습니다.)

주어진 압축 파일을 풀어보면 네 가지 파일이 있는데, pdf 파일 두개와 sign 파일 두개이다. pdf 파일은 ECHO 또는 EXECUTE로 시작하는 command를 담고 있고, sign 파일은 hex string이다.

문제 사이트에 들어가보면 payload에 대한 sign을 할 수 있는 부분과 sign된 payload를 실행할 수 있는 부분이 있다. 양쪽 모두 source code link가 제공되니 코드를 확인 해보자.

먼저, 서명을 생성하는 sign.php이다.

[...]

$file_info = $_FILES['userfile'];
check_uploaded_file($file_info);

$text = pdf_to_text($file_info['tmp_name']);
print "Extracted text: \"$text\"<br/>";

$execute_query = "EXECUTE ";
$echo_query = "ECHO ";
if (substr($text, 0, strlen($execute_query)) === $execute_query) {
  print "I don't sign EXECUTE commands. Go away.<br/>";
} else if (substr($text, 0, strlen($echo_query)) === $echo_query) {
  print "I'm OK with ECHO commands. Here is the signature: <br/>";
  $data = file_get_contents($file_info['tmp_name']);
  openssl_sign($data, $signature, $privkey);
  print bin2hex($signature);

[...]

pdf_to_text() 함수가 있는 것을 보면, sample로 주어진 것과 같은 pdf 파일을 입력으로 받아서 텍스트를 추출하는 것 같다. 그러고 난 다음에, command가 ECHO이면 sign을 생성해서 알려주고 EXECUTE이면 reject을 하도록 되어 있다. 그러니까, sign.php가 제공하는 기능은 ECHO command가 적혀있는 pdf 파일을 받아서 거기에 대한 sign을 생성해주는 것으로 생각된다.

다음은 execute.php를 살펴보자.

[...]

$data = file_get_contents($file_info['tmp_name']);
$signature = hex2bin($_POST['signature']);
if (openssl_verify($data, $signature, $pubkey)) {
  print 'Signature is OK.<br/>';
} else {
  die('Bad signature.');
}

$text = pdf_to_text($file_info['tmp_name']);
print "Text: \"$text\"<br/>";

$execute_query = "EXECUTE ";
$echo_query = "ECHO ";
if (substr($text, 0, strlen($execute_query)) === $execute_query) {
  $payload = substr($text, strlen($execute_query));
  print "About to execute: \"$payload\".<br/>";
  $out = shell_exec($payload);
  print "Output: $out";
} else if (substr($text, 0, strlen($echo_query)) === $echo_query) {
  $payload = substr($text, strlen($echo_query));
  print "About to echo: \"$payload\".<br/>";
  echo $payload;

[...]

pdf에서 command를 추출하는 부분은 sign.php와 동일하다. 다만, 추출 전에 file과 함께 입력받은 sign 값을 이용해서 서명을 검증하는 단계를 거치는 것과, 서명 검증에 성공한 경우 추출한 command를 실행해 주는 부분이 추가되어 있다.

이 문제를 풀 때는 pdf에 대한 서명을 생성하는 점에서 SHA1 collision을 이용한 문제임을 파악했다. (그 즈음에 SHA1 collision을 이용하는 CTF 문제가 몇 군데서 나왔었다.) SHA1 collision 문제임을 확인하기 위해서 php의 default signature scheme을 확인해 보았다.

역시나 default hash algorithm으로 SHA1을 사용하고 있었다.

이 문제를 해결하기 위해서는 ECHO 명령을 사용하는 pdf 문서 A와 그에 대한 sign을 생성하고, 문서 A와 동일한 sha1sum을 갖는 EXECUTE 명령을 사용하는 pdf 문서 B를 만들면 된다.

sha1 collision이 발표되고 난 후 이를 공격하기 위한 오픈소스도 여러개가 공개되었는데, 그 중에 하나를 사용하였다.

우선, echo 문서를 하나 만들고, EXECUTE ls -l . 을 수행하는 문서를 하나 만들어서 sha1sum이 동일하도록 하였다.

먼저 echo 문서를 대상으로 하여 sign을 생성하였다.

execute 문서를 echo 문서에 대한 sign과 함께 submit 하여 디렉토리의 파일 목록을 조회한 결과, flag 파일이 확인되었다.

동일한 방법으로 echo 문서 하나와 cat flag를 수행하는 execute 문서를 만들어 submit 한 결과 flag를 얻을 수 있었다.

Flag : OOO{phP_4lw4y5_d3l1v3r5_3h7_b35T_fl4g5}

'writeups > Crypto' 카테고리의 다른 글

Blind  (0) 2019.11.26
babyrsa  (0) 2019.11.26
Count me in  (0) 2019.11.26
Help Rabin  (0) 2019.11.26
Easy RSA  (0) 2019.11.25

Description

Category: Crypto

Source: CONFidence CTF 2019 Teaser

Points: 59

Author: Jisoon Park(js00n.park)

Description:

I've been lately using this cool AES-CTR, but it was super slow, so I made a parallel version. Now it's blazing fast, but for some reason I have trouble decrypting the data...

count_me_in.tar.gz

Write-up

암호화 코드와 암호화된 데이터가 주어진다.

먼저 암호화 코드를 살펴보자.

def worker_function(block):
    global counter
    key_stream = aes.encrypt(pad(str(counter)))
    result = xor_string(block, key_stream)
    counter += 1
    return result


def distribute_work(worker, data_list, processes=8):
    pool = multiprocessing.Pool(processes=processes)
    result = pool.map(worker, data_list)
    pool.close()
    return result


def encrypt_parallel(plaintext, workers_number):
    chunks = chunk(pad(plaintext), 16)
    results = distribute_work(worker_function, chunks, workers_number)
    return "".join(results)


def main():
    plaintext = """The Song of the Count

You know that I am called the Count
Because I really love to count
I could sit and count all day

[...]

""" + flag
    encrypted = encrypt_parallel(plaintext, 32)
    print(encrypted.encode("hex"))

평문의 일부가 주어져 있고, 평문을 16바이트 단위로 쪼개서 32개의 프로세스로 나누어 암호화를 하고 있다.

딱 보면 worker_function()에서 global counter를 사용하는데, multiprocess 환경에서 global 변수 사용에 대한 아무런 대비가 되어있지 않다.

어떤 일이 일어날 수 있을까. 아마 counter += 1이 제대로 반영되지 않아서 동일한 counter 값들이 사용될 수 있을 것 같다. key는 공통이니 counter가 동일하면 keystream도 동일한 것들이 있을 것이다. 진짜 그런지 살펴보자.

CTR 모드를 구현했는데, 평문을 알고 있으니 암호문과 xor 하여 key stream을 구할 수 있다. 전체 keystream의 갯수를 세어 보면 총 57개인데, 이 중에 중복을 제거해 보면 8개 keystream 밖에 남지 않는다. counter는 0부터 7까지만 사용되었다는 의미이다.

8개의 key stream을 이용해서 암호문 중 평문이 알려지지 않은 부분을 복구해보자. 암호문의 각 block을 8가지 key stream과 xor 하면서 padding이 되어있거나 printable한 문자들로만 이루어진 것들을 찾아 모으도록 exploit을 작성하였더니 flag를 얻을 수 있었다.

Flag : p4{at_the_end_of_the_day_you_can_only_count_on_yourself}

'writeups > Crypto' 카테고리의 다른 글

babyrsa  (0) 2019.11.26
Easy Pisy  (0) 2019.11.26
Help Rabin  (0) 2019.11.26
Easy RSA  (0) 2019.11.25
Decode This  (0) 2019.11.25

Description

Category: Reversing

Source: DEFCON CTF 2018 Quals.

Points: 102

Author: Jisoon Park(js00n.park)

Description:

pieces.tgz

Write-up

압축 파일을 풀어보면 broken이라는 바이너리 파일 하나와 8개의 dat 파일을 얻을 수 있다.

파이너리 파일을 실행해보면 바로 Segmentation Fault가 발생한다.

IDA로 열어보면 main() 함수와 그 위에 있는 f1(), f2(), f3(), recover_flag() 함수가 모두 'pop eax'로만 이루어져 있다. hex view로 살펴보면 파일의 해당 위치가 "X"라는 문자로만 채워져 있는 것을 볼 수 있는데, "X"를 의미하는 0x58을 disassembler가 'pop eax'로 인식한 것임을 알 수 있다.

이 부분을 dat 파일들로 적절히 채워 넣으면 될것 같다. 실제로 dat 파일들의 크기를 다 합치면 807 바이트로, broken 파일에 있는 "X"의 갯수와 동일하다.

이제 dat 파일들을 어떤 순서로 넣으면 좋을지 생각해보자. 친절하게도 정확히 f1() 함수의 시작 위치부터 main() 함수의 마지막 위치까지 비워져 있기 때문에, 첫 위치에 들어갈 파일은 함수의 prologue부터 시작해야 할테고 마지막으로 들어갈 파일은 ret로 끝나야 할 것이다.

이 조건에 맞는 파일은 다행히 각각 하나씩만 존재한다.

총 8개의 dat 파일 중에 2개의 위치는 알았으니, 나머지 6개의 위치를 특정해보자.

보통 이런 문제를 풀 때는 call이나 jmp instruction의 jump 주소를 참조하는 경우가 많지만, 이 경우이는 6가지 파일의 조합만 찾으면 되니 그냥 brute force 방식으로 조합을 시도해 보았다. (6! = 720가지 조합)

가능한 모든 조합에 대하여, 각 함수의 시작 주소에 prologue가 있는지 또는 시작 주소 바로 앞에 ret instruction이 있는지 확인해보면 될것 같다.

ret instruction을 확인하는 것을 조건으로 하여 모든 조합을 확인하였더니, 조건을 만족하는 한가지 조합이 발견되었고 그 조합을 그대로 실행하여 flag를 얻을 수 있었다.

Flag : welcOOOme

'writeups > Reversing' 카테고리의 다른 글

i can count  (0) 2019.11.26
Elementary  (0) 2019.11.26
Super Secure Vault  (0) 2019.11.26
Feed_me  (0) 2019.11.25
Snakes over cheese  (0) 2019.11.25

+ Recent posts