Web Hacking/WriteUp

[CCE 2024] Advanced Login System WriteUp

프레딕 2024. 8. 7. 00:05
728x90

이번 CCE 예선에서 solver가 0명인 문제이다

나도 한 두시간? 정도 잡아서 풀어봤는데 첫번째 필터링조차 우회하지 못하고 포기했다....

끝나고 Writeup이 올라왔길래 한번 정리해서 작성해본다.

// admin_login.php
<?php

    session_start();
    require_once "lib.php";
    require_once "config/dbconn.php";

    if (isset($_POST["username"]) && isset($_POST["password"])) {
        $query = "SELECT userid, password FROM user WHERE userid = '".bin2hex($_POST["username"])."' and password = '".bin2hex($_POST["password"])."';";
        $data = Array();
        try {
            $result = $mysqli->query($query);
            $data = mysqli_fetch_array($result);
        } catch(Exception $e) {
        }
        isFirstLoginAttempt();
        if (isset($data) && $data[0] === bin2hex($_POST["username"]) && $data[1] === bin2hex($_POST["password"]) && !$_SESSION['first_attempt']) {
            session_write("isLogin", true);
            if ($_POST["username"] === "admin" && $_SESSION['first_attempt']) {
                
                $_SESSION["level"] = 99999;
                $_SESSION["username"] = "admin";
                header("Location: /index.php");
            } else {
                print_error("Smart admins never enter the wrong password :p");
            }
        } else {
            print_error("Incorrect Password");
        }
    } else {
        header("Location: /index.php");
    }
?>
// lib.php
<?php
    error_reporting(0);


    function global_filter($input) {
        $pattern = '/[^a-zA-Z0-9\/\-]/';

        if (is_array($input)) {
            foreach ($input as $key => $value) {
                global_filter($value); 
            }
        } else {
            if (preg_match($pattern, $input)) {
                die("Invalid character");
            }
        }
    }

    function session_write($key, $value) {
        $_SESSION[$key] = $value;
        session_commit();
        session_start();
    }

    function isFirstLoginAttempt() {
        if (!isset($_SESSION["first_attempt"])) {
            session_write("first_attempt", true);
        } else {
            session_write("first_attempt", false);
        }
    }

    function print_error($sting) {
        die($sting);
    }

    function debug($arr) {
        var_dump(call_user_func($arr[0],$arr[1]));
    }

    global_filter($_REQUEST);
?>
INSERT INTO user (userid, password) VALUES ('61646d696e', '737570657253656372657441646d696e50617373776f72642140233132');
// admin , superSecretAdminPassword!@#12

 

먼저 친절하게 admin 아이디를 알려주신다.

16진수로 되어있기에 문자로 디코딩해보면 위와 같은 admin 아이디를 얻을 수 있다.

 

그다음 admin_login.php를 보면은 username과 password로 admin id를 로그인 한다음 session의 first_attempt값이 false이면은 통과한 후 다시 admin이고 session의 first_attempt값이 true이면 admin계정으로 세션을 바꿔준다.

여기서 통과해야할 필터링은 다음과 같이 두가지이다.

 

1. lib.php에서 $_REQUEST 값에 영문자와 숫자만 쓸수 있게 필터링하는 $pattern 우회

2. session의 first_attempt값 우회

 

일단 첫번째 부터 알아보겠다.

lib.php에선 $_REQUEST값을 조사한 후 영문자와 숫자로만 이루어지지 않으면 invalid character라고 필터링 된다.

이때 admin_login.php에선 post값을 사용하고 lib.php에선 REQUEST값을 필터링한다는 점을 사용해 우회해야 한다.

이게 뭔말이냐면, 일단 아래와 같은 url에서 필터링 방법을 확인할 수 있다.

https://stackoverflow.com/questions/43157933/what-is-the-request-precedence

 

What is the $_REQUEST precedence?

PHP Superglobal variables PHP has global variables which can be accessed within any scope of your script. Three of these variables ($_GET, $_POST, $_COOKIE) are stored within a fourth variable ($

stackoverflow.com

만약? GET과 POST와 COOKIE에 똑같은 이름으로 된 파라미터들이 존재한다면 $_REQUEST 값은

variables_order = "GPC" 의 순서에 따라 지정된다.

즉, GET이 먼저 지정되고 그다음 POST 그다음 COOKIE로 지정된다.

그렇다면 만약 셋다 똑같은 이름으로 된 파라미터들이 존재한다면 $_REQUEST값은 처음에 GET 파라미터 값으로 저장되었다가 그다음 POST body값으로 덮어씌워지고 마지막에 COOKIE값으로 덮어씌워줘 최종적으로 COOKIE값이 저장된다.

말이 살짝 어려운거같은데 만약 POST와 COOKIE 만 같은 이름으로된 파라미터들이 들어왔다고 하자.

그러면 $_REQUEST값이 POST body값으로 되었다가 COOKIE값으로 덮어씌워져 결국엔 $_REQUEST값엔 COOKIE값들이 있을것이다.

그러면 만약 COOKIE값에 정상적인 값을 넣고 POST값엔 특수문자를 넣으면? 저 lib.php의 필터링을 우회할 수 있다.

그래서 post값에 username : admin, password : superSecretAdminPassword!@#12를 넣고,

cookie값엔 멀쩡한값 예를들어 username:1, password:1 이렇게 넣으면 가볍게 필터링을 통과 가능하다.

 

그다음 두번째 필터링이다.

if (isset($data) && $data[0] === bin2hex($_POST["username"]) && $data[1] === bin2hex($_POST["password"]) && !$_SESSION['first_attempt']) {
            session_write("isLogin", true);
            if ($_POST["username"] === "admin" && $_SESSION['first_attempt']) {
                $_SESSION["level"] = 99999;
                $_SESSION["username"] = "admin";
                header("Location: /index.php");
            } else {
                print_error("Smart admins never enter the wrong password :p");
            }
        } else {
            print_error("Incorrect Password");
        }

저 first_attempt세션값을 통과해야한다. 첫번째 if문에선 first_attempt값이 false여야하고 두번째 if문에선 true여야한다.

그런데 session_write함수는 isLogin값을 true로 바꿔주니.. 남은 시도해볼만한 방법은 race condition이다.

session_write에서 race condition이 일어날만한 짓을 트리거한다음 first_attempt값이 true인 페이로드로 중간에 요청을 전송하면 뚫릴것이다.

그렇다면 race condition 트리거는 어떻게 하느냐?

일반적인 방법으론 불가능하겠지만 만약 php session upload progress를 악용해서 race condition을 유발시킬 수 있다. 만약 post방식으로 전달된 php_session_upload_progress 파라미터 값이 session키에 적용될때 특수문자를 삽입 가능한데

만약 |를 전달할 경우엔 session parsing과정에서 broken session으로 판단하고 세션값을 전부 삭제하게 된다.

이때 race condition이 발생하고 이걸 이용해 우회할 수 있다...

그런데 실제 환경에서 여러번 해봤는데 이부분은 우회가 잘 안돼서... 만약 성공하면 다시 이어서 작성하겠다.

728x90
반응형