2019*CTF之Web部分題解
這道題目比較有意思, Web+Pwn ,用 PHP 寫了一個模擬資料進出棧的過程。程式原始碼經過 enphp 加密,當中有很多字元亂碼,而且許多變數名也經過混淆。這裡可以直接用 var_export 匯出全域性變數,或者直接用IDE動態除錯。
我們可以看到一個變數置換表。通過編寫程式進行自動替換,可生成美化後的程式碼。替換指令碼如下:
<?php include 'index.php';// index.php為被加密的檔案 function replaceString($match){ global $O; $index = -1; if(false !== strpos($match[1], 'x')){ $index = hexdec($match[1]); } else{ $index = $match[1]; } if( function_exists($O[$index]) || class_exists($O[$index]) || defined($O[$index]) ){ return $O[$index]; } else{ return "'".addslashes($O[$index])."'"; } } $encrypto_string = file_get_contents('index.php'); $encrypto_string = preg_replace_callback('/\$GLOBALS[\[\{]O0[\]\}][\[\{](\w{1,15})[\]\}]/','replaceString',$encrypto_string); $decrypto_string = preg_replace_callback('/\$[O0]*[\[\{](\w{2,15})[\]\}]/','replaceString',$encrypto_string); file_put_contents('decrypted.php', $decrypto_string); ?>
然後手動刪掉亂碼字元,最後生成的程式碼如下:
<?php error_reporting(E_ALL^E_NOTICE); define('O0', 'O'); require_once 'sandbox.php'; $seed = time(); srand($seed); define(INS_OFFSET,rand(0x0000,0xffff)); $regs = array( 'eax'=>0x0, 'ebp'=>0x0, 'esp'=>0x0, 'eip'=>0x0, ); function aslr(&$O00,$O0O){ $O00 = $O00 + 0x60000000 + INS_OFFSET + 0x001 ; } $func_ = array_flip($func); array_walk($func_,aslr); $plt = array_flip($func_); function handle_data($OOO){ $OO0O=&$GLOBALS{O0}; $O000 = strlen($OOO); $O00O = $O000/0x000004+(0x001*($O000%0x000004)); $O0O0 = str_split($OOO,0x000004); $O0O0[$O00O-0x001] = str_pad($O0O0[$O00O-0x001],0x000004,'\0'); foreach ($O0O0as$O0OO=>&$OO00){ $OO00 = strrev(bin2hex($OO00)); } return $O0O0; } function gen_canary(){ $O0O00=&$GLOBALS{O0}; $OOOO = 'abcdefghijklmnopqrstuvwxyzABCDEFGHJKLMNPQEST123456789'; $O0000 = $OOOO[rand(0,strlen($OOOO)-0x001)]; $O000O = $OOOO[rand(0,strlen($OOOO)-0x001)]; $O00O0 = $OOOO[rand(0,strlen($OOOO)-0x001)]; $O00OO = '\0'; return handle_data($O0000.$O000O.$O00O0.$O00OO)[0]; } $canary = gen_canary(); $canarycheck = $canary; function check_canary(){ global $canary; global $canarycheck; if($canary != $canarycheck){ die('emmmmmm...Don\'t attack me!'); } } Class O0OO0{ private$ebp,$stack,$esp; publicfunction __construct($O0OOO,$OO000) { $OO00O=&$GLOBALS{O0}; $this->stack = array(); global $regs; $this->ebp = &$regs['ebp']; $this->esp = &$regs['esp']; $this->ebp = 0xfffe0000 + rand(0x0000,0xffff); global $canary; $this->stack[$this->ebp - 0x4] = &$canary; $this->stack[$this->ebp] = $this->ebp + rand(0x0000,0xffff); $this->esp = $this->ebp - (rand(0x20,0x60)*0x000004); $this->stack[$this->ebp + 0x4] = dechex($O0OOO); if($OO000 != NULL) $this->{'pushdata'}($OO000); } publicfunction pushdata($OO0O0){ $OOO00=&$GLOBALS{O0}; $OO0O0 = handle_data($OO0O0); for($OO0OO=0;$OO0OO<count($OO0O0);$OO0OO++){ $this->stack[$this->esp+($OO0OO*0x000004)] = $OO0O0[$OO0OO]; //no args in my stack haha check_canary(); } } publicfunction recover_data($OOO0O){ $OOOO0=&$GLOBALS{O0}; return hex2bin(strrev($OOO0O)); } publicfunction outputdata(){ $O0000O=&$GLOBALS{O0}; global $regs; echo 'root says: '; while(0x001){ if($this->esp == $this->ebp-0x4) break; $this->{'pop'}('eax'); $OOOOO = $this->{'recover_data'}($regs['eax']); $O00000 = explode('\0',$OOOOO); echo $O00000[0]; if(count($O00000)>0x001){ break; } } } publicfunction ret(){ $O000O0=&$GLOBALS{O0}; $this->esp = $this->ebp; $this->{'pop'}('ebp'); $this->{'pop'}('eip'); $this->{'call'}(); } publicfunction get_data_from_reg($O000OO){ $O00OO0=&$GLOBALS{O0}; global $regs; $O00O00 = $this->{'recover_data'}($regs[$O000OO]); $O00O0O = explode('\0',$O00O00); return $O00O0O[0]; } publicfunction call(){ $O0OO00=&$GLOBALS{O0}; global $regs; global $plt; $O00OOO = hexdec($regs['eip']); if(isset($_REQUEST[$O00OOO])) { $this->{'pop'}('eax'); $O0O000 = (int)$this->{'get_data_from_reg'}('eax'); $O0O00O = array(); for($O0O0O0=0;$O0O0O0<$O0O000;$O0O0O0++){ $this->{'pop'}('eax'); $O0O0OO = $this->{'get_data_from_reg'}('eax'); array_push($O0O00O,$_REQUEST[$O0O0OO]); } call_user_func_array($plt[$O00OOO],$O0O00O); } else { call_user_func($plt[$O00OOO]); } } publicfunction push($O0OO0O){ $O0OOOO=&$GLOBALS{O0}; global $regs; $O0OOO0 = $regs[$O0OO0O]; if( hex2bin(strrev($O0OOO0)) == NULL ) die('data error'); $this->stack[$this->esp] = $O0OOO0; $this->esp -= 0x000004; } publicfunction pop($OO0000){ global $regs; $regs[$OO0000] = $this->stack[$this->esp]; $this->esp += 0x000004; } publicfunction __call($OO000O,$OO00O0){ check_canary(); } }class_alias(O0OO0,stack,0);print_R(O0OO0);print_R(stack); if(isset($_POST['data'])) { $phpinfo_addr = array_search(phpinfo, $plt); $gets = $_POST['data']; $main_stack = new stack($phpinfo_addr, $gets); echo '--------------------output---------------------</br></br>'; $main_stack->{'outputdata'}(); echo '</br></br>------------------phpinfo()------------------</br>'; $main_stack->{'ret'}(); }
接下來我們來分段看這些函式的功能,程式碼當中還是有很多 0o 字元變數,我會將它們替換成其他有含義的變數名。
首先程式用 $regs 來模擬 eax、ebp、esp、eip 4個暫存器,然後定義了 aslr 函式模擬地址空間隨機化,但是這個隨機化的過程存在問題,因為用了可預測的變數做隨機數種子,這樣地址就會被計算出來。 handle_data 函式會對資料進行填充,用 \x00 將其長度填充成4的倍數,然後再以4長度分割。但是這個程式碼分割方式存在問題,例如我們輸入的資料為 123 ,按照正常的邏輯經過處理後,資料應該變成 123\x00
,但是這裡的 handle_data 函式卻會將其處理成 123\x00\x00\x00\x00\x00
(多了4個 \x00
)。
gen_canary函式使用者生成用於防止棧溢位的 canary 字串,程式碼如下:
接著通過 O0OO0 類的構造建立棧幀,其過程如下圖右邊所示:
接著開始將資料壓棧,但是這裡的 pushdata 方法貌似寫的有問題,先進棧資料的地址應該要比後進棧的資料地址高,即 push 中的寫法是正確的,但是卻沒呼叫。回到壓棧操作本身,可以發現程式並未對資料的長度進行判斷,這樣有可能會導致資料過長而覆蓋原有的資料。雖然有 canary 的保護,但是這個 canary 是可以洩露的,我們完全可以繞過。
完成資料壓棧之後,開始進行資料出棧操作。程式會將 canary 到棧頂之間的所有資料都彈出去,之後結束彈棧操作。
完成資料出棧操作後,開始模擬 ret 指令,彈出下一條指令的地址並呼叫。 eip 上存放的是 phpinfo 函式的地址,但是這個地址可以利用前面 pushdata 可以覆蓋成我們想要的函式地址。然後根據 REQUEST 請求引數中是否含有這個函式地址,分別進行有參函式、無參函式的呼叫。
這個時候,我們就可以執行任意函數了。但是從 phpinfo 的資訊可以看出程式設定了 disable_functions :
file_get_contents,file_put_contents,fwrite,file,chmod,chown,copy,link,fflush,mkdir,popen,rename,touch,unlink,pcntl_alarm,move_upload_file,pcntl_fork,pcntl_waitpid,pcntl_wait,pcntl_wifexited,pcntl_wifstopped,pcntl_wifsignaled,fsockopen,pfsockopen,pcntl_wifcontinued,pcntl_wexitstatus,pcntl_wtermsig,curl_init,curl_exec,curl_multi_init,curl_multi_exec,dba_open,dba_popen,pcntl_wstopsig,pcntl_signal,pcntl_signal_get_handler,pcntl_signal_dispatch,pcntl_get_last_error,pcntl_strerror,pcntl_sigprocmask,pcntl_sigwaitinfo,pcntl_sigtimedwait,pcntl_exec,pcntl_getpriority,pcntl_setpriority,pcntl_async_signals,system,exec,shell_exec,popen,proc_open,passthru,symlink,link,syslog,imap_open,ld,mail,dl,putenv
然而沒有禁用 create_function 函式,我們便可以利用該函式實現任意程式碼執行,具體可以參考: create_function函式如何實現RCE 。但是即便這樣,我們也沒辦法繞過 disable_functions 執行 readflag 程式。
這時,可以把題目給的 dockerfile 拿來跑一下,看下其中開放的服務等等。我們會發現題目環境中開啟了預設安裝的 php-fpm 服務,這樣我們可以通過 unix sock 與 php-fpm 進行通訊。
其中,很重要的一點是,當我們使用 unix sock 與 php-fpm 進行通訊,應用在PHP的配置檔案是 php-fpm 獨立的 php.ini 檔案,而這個檔案中預設 disable_functions 限制就少了很多。我們在題目上看到的 disable_functions ,實際上是 apache 自己的 php.ini 配置。這樣,我們就可以通過 unix sock 與 php-fpm 進行通訊,從而繞過 disable_functions 限制。
當我們通過 unix sock 與 php-fpm 進行通訊,又會發現不管向 index.php、sandbox.php 哪個檔案請求,都會觸發如下程式碼:
我們可以通過搜尋環境中預設安裝 PHP 後存在的 PHP 檔案,利用他們作為請求檔案即可繞過上面程式碼。
然而實際上, PHP 中並不允許通過 ini_set 函式來設定 disable_functions 的值,即便你設定了也不會生效。
由於 apache 模式下設定的 disable_functions 沒有禁用 stream_socket_client、stream_socket_sendto 這兩個函式。我們可以利用其進行 SSRF ,與 PHP-FPM 進行通訊,最後按照 fastcig 請求格式構造報文即可。在看官方 exploit.php 時,發現其通過 PHP_VALUE 設定了 disable_functions=空
,而實際上 disable_functions 這個選項是PHP載入的時候就確定了,並不能通過 PHP_VALUE 設定生效。
mywebsql
這道題比較簡單,直接利用admin/admin就可以登入mywebsql管理介面。然後利用mywebsql3.7的RCE直接寫shell,具體參考: https://github.com/eddietcc/CVEnotes/blob/master/MyWebSQL/RCE/readme.md
之後要執行readflag程式,而這個程式會先給你一串表示式,你要快速的告訴他答案,答對即可獲得flag。這裡我使用如下PHP程式獲得flag:
<?php $descriptorspec = array( 0 => array("pipe", "r"),// 標準輸入,子程序從此管道中讀取資料 1 => array("pipe", "w"),// 標準輸出,子程序向此管道中寫入資料 2 => array("file", "/tmp/error-output.txt", "a") // 標準錯誤,寫入到一個檔案 ); $process = proc_open('/readflag', $descriptorspec, $pipes, $cwd, $env); if (is_resource($process)) { $question = fread($pipes[1],1024); // 獲取程式問題 $question = fread($pipes[1],1024); // 獲取程式問題 $question = trim($question); var_dump($question); eval('$result = '.$question.';');// 計算問題結果 fwrite($pipes[0], $result);// 回答程式問題 fclose($pipes[0]); var_dump($result); // $flag = stream_get_contents($pipes[1]);// getflag $flag = fread($pipes[1],1024); $flag = fread($pipes[1],1024); $flag = fread($pipes[1],1024); fclose($pipes[1]); var_dump($flag); $return_value = proc_close($process); echo "command returned $return_value\n"; } ?>
賽後還看到有選手逆向readflag程式,然後bypass ualarm函式的,具體參考 https://www.zhaoj.in/read-5479.html