题目被大佬非预期的注入成功了,膜大佬,tql ..逃 。。。。
非预期解也挺有意思的,详情可以去看https://xz.aliyun.com/t/3428,我这里只说一下预期解法吧。
题目的docker环境已经上传到https://github.com/wonderkun/CTF_web/blob/master/web400-12/README.md?1543386212265了,感兴趣的可以去下载。
下面是题解:
首先看session的处理类:
# file: include/MySessionHandle.php
public function write($session_id,$data){
$time = time();
$res = $this->dbsession->query("SELECT * FROM `{$this->dbsession->table_name}` where `sessionid` = '{$session_id}' ");
if($res){
$this->dbsession->execute("UPDATE `{$this->dbsession->table_name}` SET `data` = '{$data}',`lastvisit` = '{$time}' where `sessionid` = '{$session_id}'");
}else{
$res = $this->dbsession->create(
["data"=>$data,
"sessionid"=>$session_id,
"lastvisit"=>$time]);
}
return true;
}
这里的 update
操作没有对 $data
进行 addslashes 操作,所以是有问题的。
虽然前面有简易的waf,对用户的输入进行了过滤:
function escape(&$arg) {
if(is_array($arg)) {
foreach ($arg as &$value) {
escape($value);
}
} else {
$arg = str_replace(["'", '\\', '(', ')'], ["‘", '\\\\', '(', ')'], $arg);
}
}
执行到这里 $data
已经不可能再单个出现 \
,'
和(
,)
了,所以是不存在注入的(不考虑那个非预期解的情况),但是要注意是 $data
是序列化之后的数据,存在一个长度的问题。
假如说 $data
是 s:6:"test\\"
(因为在对 Session 类进行序列化之前,已经对所有成员用 escape 函数处理过一次), 那么写入数据库之后就变成了 s:6:"test\"
, 那么在执行反序列化的时候,就会导致后面一个字符被吞掉。
先来看一下数据库中存储的 session 的格式:
data|s:288:"O:7:"Session":4:{s:11:" Session ip";s:10:"172.18.0.1";s:18:" Session userAgent";s:128:"Mozilla/5.0 ?Macintosh; Intel Mac OS X 10_14_0? AppleWebKit/537.36 ?KHTML, like Gecko? Chrome/69.0.3497.92 Safari/537.36";s:15:" Session userId";s:1:"1";s:18:" Session loginTime";i:1543310132;}";username|s:9:"wonderkun";
利用上面说的那个问题,我们可以在 data 字段注入 \
,向后面吞字符,吞掉后面的 username字段,然后在 username 值的位置布局我们想要的数据,来伪造session。
php 处理 session 的时候,有一个机制是这样的,后面的值会覆盖掉前面的值,例如:
data|s:5:"guest";data|s:5:"admin";
那么在获取 $_SESSION['data']
的时候,后面的值就会覆盖掉前面的值,得到的是 admin
。
通过上面的分析,思路就明确了,我们注册的用户名可以是我们需要伪造的 $_SESSION['data']
的序列化内容, 然后在真正的 $_SESSION['data']
位置引入 \
,让他吞掉后面的字符 ";username|s:9:"
, 实现伪造 $_SESSION['data']
的值 ,得到一次反序列化的机会。
通过上面的分析,我们可以任意伪造 $_SESSION['data']
的内容,并且可以获取到一次反序列化的机会。但是题目的代码限制了反序列化:
$session = unserialize($_SESSION["data"],["allowed_classes" => ["Session"]]);
这里仅仅可以反序列化出来 Session
类,所以这里是没有办法实现任意任意类伪造,所以是没有办法构造 pop chains
的。
但是,看代码发现有一个静态的函数调用:
$this->now = $session::getTime(time());
php是动态语言,是支持动态的函数调用的,比如说:
$a = phpinfo; $a();
我们可以扩展一下思考,示例代码如下:
class A{
public static function test(){
echo "hacked";
}
}
class B{
public $name = "i am b";
}
unserialize(serialize("A"),["allowed_classes" => ["B"]])::test();
// hacked
也就是说我们其实是可以通过字符串的方式来调用一个类的类方法,等价于下面的方式:
$a = "A"; $a::test();
通过这种方式,我们就可以在没有反序列出来 A
类的情况下,利用字符串"A"调用 A
类的静态方法。
但是我们能调用哪个类的静态方法呢? 也没有哪个php内置类有 getTime
这个静态方法吧 ?
$__module = isset($_GET['m']) ? strtolower($_GET['m']) : '';
$__controller = isset($_GET['c']) ? strtolower($_GET['c']) : 'main';
$__action = isset($_GET['a']) ? strtolower($_GET['a']) : 'index';
$__custom = isset($_GET['s']) ? strtolower($_GET['s']) : 'custom';
spl_autoload_register('inner_autoload');
function inner_autoload($class){
// echo $class."\n";
GLOBAL $__module,$__custom;
$class = str_replace("\\","/",$class);
foreach(array('model','include','controller'.(empty($__module)?'':DS.$__module),$__custom) as $dir){
$file = APP_DIR.DS.$dir.DS.$class.'.php';
if(file_exists($file)){
include $file;
return;
}
}
}
看到这些代码,那么思路就串起来了,我们可以通过刚才伪造的字符串来调用一个类,然后就会触发一次类加载,
然后类加载器就会在我们制定的目录下寻找这个类的代码,并包含进来。只需要控制 s=img/upload/,就能实现包含我们上传的php文件。
\\\\\\\\\\\\\\\\
16个反斜杠,根据自己的情况调整。