이번 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
만약? 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이 발생하고 이걸 이용해 우회할 수 있다...
그런데 실제 환경에서 여러번 해봤는데 이부분은 우회가 잘 안돼서... 만약 성공하면 다시 이어서 작성하겠다.
'Web Hacking > WriteUp' 카테고리의 다른 글
ASIS CTF 2024 Web Writeup (0) | 2024.09.22 |
---|---|
WhiteHat School 2nd CTF SSH Tunneling Final Writeup (0) | 2024.09.05 |
Project Sekai CTF 2024 Web Writeup (0) | 2024.08.24 |
[CCE 2024] Web WriteUp (0) | 2024.08.05 |
[Sechack.kr] Shop WriteUp (0) | 2024.07.19 |