目录
0x00代码分析
0x01FILTER_VALIDATE_EMAIL绕过
0x02NginxHost限制绕过
0x03SQL注入
框架源码
链接:https://pan.baidu.com/s/1CXYi9i8L7hPmWEtIGBqD_Q?pwd=uzji
提取码:uzji
过滤函数escape,过滤了单引号,括号,反斜线
function escape(&$arg) {
if(is_array($arg)) {
foreach ($arg as &$value) {
escape($value);
}
} else {
$arg = str_replace(["'", '\\', '(', ')'], ["‘", '\\\\', '(', ')'], $arg);
}
}
全局过滤escape函数,过滤request,post,get,那这仨应该没戏了
escape($_REQUEST);
escape($_POST);
escape($_GET);
$controller_name = $__controller.'Controller';
$action_name = 'action'.$__action;
看到这个email拼接,SQL注入应该有戏
$email = arg('email');
if (empty($email)) {
$email = $username . '@' . arg('HTTP_HOST');
}
if (!filter_var($email, FILTER_VALIDATE_EMAIL)) {
$this->error('Email error.');
}
$ret = $user->create([
'username' => $username,
'password' => md5($password),
'email' => $email
如果email为空,email变量就由username+@+HTTP_HOST组成,这两个变量我们都可控
但是还有一个PHP自带的FILTER_VALIDATE_EMAIL过滤
如果我们成功绕过这个过滤,那就可以闭合单引号让SQL报错
RFC 3696规定,邮箱地址分为local part和domain part两部分。local part中包含特殊字符,需要如下处理:
将特殊字符用\
转义,如Joe\'[email protected]
或将local part包裹在双引号中,如"Joe'Blow"@example.com
local part长度不超过64个字符
虽然PHP没有完全按照RFC 3696进行检测,但支持上述第2种写法。所以,我们可以利用之绕过FILTER_VALIDATE_EMAIL
的检测。
这种绕过方式是可行的
但是local part长度不能超过64个字符
依照刚刚的想法,抓包发送
发现报404,分析下
如果服务器上配置了虚拟主机,那当http请求来到Nginx时,Nginx会依据请求头中的host字段来判断该请求应该由哪一个虚拟主机来解析,也就是转到哪个根目录下
如下,如果host为myweb.com,那就会转到html/site/web这个目录下
所以我们修改了host后,Nginx找不到虚拟主机的根目录,就会转到默认配置下,而默认配置中没有/main/register这个目录,所以就显示了404not found
这个问题有三种绕过方式:
1.Nginx在处理Host的时候,会将Host用冒号分割成hostname和port,port部分被丢弃。
所以,我们可以设置Host的值为 myweb.com:aaa'"@qq.com ,这样就能访问到目标Server块:
成功闭合单引号,存在SQL注入
2.当我们传入两个Host头的时候,Nginx将以第一个为准,而PHP-FPM将以第二个为准。
这样就完美解决了我们的问题,既能让Nginx找到对应的server块,又能让我们的payload传入后端
但此绕过方法需要Nginx在1.20版本以下
3.利用https的SNI字段来让Nginx找到对应的server块
那什么是SNI(Server Name Indication)呢
早期的SSLv2根据经典的公钥基础设施PKI(Public Key Infrastructure)设计,默认一台服务器(或者说一个IP)只会提供一个服务,所以在SSL握手时,服务器端可以确信客户端申请的是哪张证书。
但是让人万万没有想到的是,虚拟主机大力发展起来了,这就造成了一个IP会对应多个域名的情况。解决办法有一些,例如申请泛域名证书,对所有*.yourdomain.com的域名都可以认证,但如果你还有一个yourdomain.net的域名,那就不行了。
在HTTP协议中,请求的域名作为主机头(Host)放在HTTP Header中,所以服务器端知道应该把请求引向哪个域名,但是早期的SSL做不到这一点,因为在SSL握手的过程中,根本不会有Host的信息,所以服务器端通常返回的是配置中的第一个可用证书。因而一些较老的环境,可能会产生多域名分别配好了证书,但返回的始终是同一个。
既然问题的原因是在SSL握手时缺少主机头信息,那么补上就是了。
SNI(Server Name Indication)定义在RFC 4366,是一项用于改善SSL/TLS的技术,在SSLv3/TLSv1中被启用。它允许客户端在发起SSL握手请求时(具体说来,是客户端发出SSL请求中的ClientHello阶段),就提交请求的Host信息,使得服务器能够切换到正确的域并返回相应的证书。
要使用SNI,需要客户端和服务器端同时满足条件,幸好对于现代浏览器来说,大部分都支持SSLv3/TLSv1,所以都可以享受SNI带来的便利。
所以综上,在https中,ssl可以提前发送Host字段让Nginx识别到正确的域,返回正确的证书
在bp右上角的target旁边可以修改Host信息,让其包含在ClientHello中
所以请求头里的host字段我们便可以修改成我们的payload了
按照上面的方法我们成功触发了SQL的报错信息,那就再继续构造SQL注入的语句
这个SQL注入是insert语句注入,并且注意是有字符限制的,超过就会触发email error错误
原insert语句:
insert into users (username,password,email) values('$username','$password','$email');
我们现在的payload传入后变为:
"x@aaa'"@qq.com
既然单引号可以闭合,那我们就可以闭合values,在后面再添加一个values值
values('$username','$password', ' "x@') , ('flag','md5(123456)',(select(flag)from(flags)));#" @qq.com')
email变量由username+@+HTTP_HOST组成
这里 username="x
+@+
host=') , ('flag','md5(123456)',(select(flag)from(flags)));#" @qq.com
成功注册,登录我们刚刚输入的用户名和密码,拿到flag