JavaWeb网页截图中的ssrf

一、相关背景

  有些网站需求提供网页截图功能,例如反馈意见时需要带上屏幕截图,又或者说将项目中统计报表的界面的数据定时发送等。部分情况下是使用PhantomJs实现,但是存在退出进程无法清理干净、容易被反爬虫等问题。同时Phantomjs已经目前也已经停止更新与维护。

  Headerless Browser(无头的浏览器)是浏览器的无界面状态,可以在不打开浏览器GUI的情况下,使用浏览器支持的性能。而Chrome Headless相比于其他的浏览器,可以更便捷的运行web自动化,编写爬虫、截图等。十分方便的满足了网页截图的业务需要。

wKg0C2Cxtr2ABxIAAABZWtBY4E759.png

二、selenium+chrome headless

  Selenium 是一个用于 Web 应用程序测试的工具。它的优点在于,浏览器能打开的页面,使用 selenium 就一定能获取到。配合chrome headless可以很好的完成网页截图的业务功能。

  相关依赖:

    <dependency>
        <groupId>org.seleniumhq.selenium</groupId>
        <artifactId>selenium-java</artifactId>
    </dependency>

  安装完chrome headless,并在代码中指定chromedriver驱动后就可以使用了:

        // 设置驱动地址
        System.setProperty("webdriver.chrome.driver", "/chromedriver");
        ChromeOptions options = new ChromeOptions();
        // 设置谷歌浏览器exe文件所在地址
        options.setBinary("C:\\Users\\qizhan\\AppData\\Local\\Google\\Chrome\\Application\\chrome.exe");
        // 这里是要执行的命令,如需修改截图页面的尺寸,修改--window-size的参数即可
        options.addArguments("--headless", "--disable-gpu", "--window-size=1920,1200", "--ignore-certificate-errors");
        ChromeDriver driver = new ChromeDriver(options);
        // 访问页面
        driver.get("http://sec-in.com");
        //执行脚本
        String js1 = "return document.body.clientHeight.toString()";
        String js1_result = driver.executeScript(js1) + "";
        int height = Integer.parseInt(js1_result);
        driver.manage().window().setSize(new Dimension(830, height + 100));
        // 页面等待渲染时长,如果你的页面需要动态渲染数据的话一定要留出页面渲染的时间,单位默认是秒
        Wait<WebDriver> wait = new WebDriverWait(driver, 3);
        wait.until(new ExpectedCondition<WebElement>() {
            public WebElement apply(WebDriver d) {
                // 等待前台页面中 id为“kw”的组件渲染完毕,后截图
                // 若无需等待渲染,return true即可。 不同页面视情况设置id
                return d.findElement(By.id("app"));
            }
        });
        // 获取到截图的文件
        File screenshotFile = ((TakesScreenshot)driver).getScreenshotAs(OutputType.FILE);

  通过上面简单的配置就可以获取到对应网页的截图了。得到screenshotFile后可以根据实际的业务场景进行图片的上传、邮件发送等功能的实现。可以看到,具体的截图实现实际上是通过传入对应的url进行处理的。跟所有的ssrf漏洞一样,如果没有相关的安全措施,会存在安全风险。

三、代码分析

  查看selenium中driver.get()方法的具体实现:

  主要调用的org.openqa.selenium.remote.RemoteWebDriver的get方法:

    public void get(String url) {
        this.execute("get", ImmutableMap.of("url", url));
    }

  查看execute方法的具体实现:

protected Response execute(String driverCommand, Map<String, ?> parameters) {
        Command command = new Command(this.sessionId, driverCommand, parameters);
        long start = System.currentTimeMillis();
        String currentName = Thread.currentThread().getName();
        Thread.currentThread().setName(String.format("Forwarding %s on session %s to remote", driverCommand, this.sessionId));

        Response response;
        try {
            this.log(this.sessionId, command.getName(), command, RemoteWebDriver.When.BEFORE);
            response = this.executor.execute(command);
            this.log(this.sessionId, command.getName(), command, RemoteWebDriver.When.AFTER);
            Object value;
            if (response == null) {
                value = null;
                return (Response)value;
            }

            value = this.converter.apply(response.getValue());
            response.setValue(value);
        } catch (SessionNotFoundException var17) {
            throw var17;
        } catch (Exception var18) {
            this.log(this.sessionId, command.getName(), command, RemoteWebDriver.When.EXCEPTION);
            String errorMessage = "Error communicating with the remote browser. It may have died.";
            if (driverCommand.equals("newSession")) {
                errorMessage = "Could not start a new session. Possible causes are invalid address of the remote server or browser start-up failure.";
            }

            UnreachableBrowserException ube = new UnreachableBrowserException(errorMessage, var18);
            if (this.getSessionId() != null) {
                ube.addInfo("Session ID", this.getSessionId().toString());
            }

            if (this.getCapabilities() != null) {
                ube.addInfo("Capabilities", this.getCapabilities().toString());
            }

            throw ube;
        } finally {
            Thread.currentThread().setName(currentName);
        }

        try {
            this.errorHandler.throwIfResponseFailed(response, System.currentTimeMillis() - start);
        } catch (WebDriverException var16) {
            if (parameters != null && parameters.containsKey("using") && parameters.containsKey("value")) {
                var16.addInfo("*** Element info", String.format("{Using=%s, value=%s}", parameters.get("using"), parameters.get("value")));
            }

            var16.addInfo("Driver info", this.getClass().getName());
            if (this.getSessionId() != null) {
                var16.addInfo("Session ID", this.getSessionId().toString());
            }

            if (this.getCapabilities() != null) {
                var16.addInfo("Capabilities", this.getCapabilities().toString());
            }

            Throwables.propagate(var16);
        }

        return response;
    }

  相关的URL参数封装在parameters中进行传输,首先封装在Command对象中,然后再调用DriverCommandExecutor的execute方法:

Command command = new Command(this.sessionId, driverCommand, parameters);
public Response execute(Command command) throws IOException {
        if ("newSession".equals(command.getName())) {
            this.service.start();
        }

        Response var2;
        try {
            var2 = super.execute(command);
        } catch (Throwable var7) {
            Throwable rootCause = Throwables.getRootCause(var7);
            if (rootCause instanceof ConnectException && "Connection refused".equals(rootCause.getMessage()) && !this.service.isRunning()) {
                throw new WebDriverException("The driver server has unexpectedly died!", var7);
            }

            Throwables.propagateIfPossible(var7);
            throw new WebDriverException(var7);
        } finally {
            if ("quit".equals(command.getName())) {
                this.service.stop();
            }

        }

        return var2;
    }

  然后调用了HttpCommandExecutor中的execute方法,Comand对象中包含了之前相关的请求参数,包括之前的URL:

public Response execute(Command command) throws IOException {
        if (command.getSessionId() == null) {
            if ("quit".equals(command.getName())) {
                return new Response();
            }

            if (!"getAllSessions".equals(command.getName()) && !"newSession".equals(command.getName())) {
                throw new SessionNotFoundException("Session ID is null. Using WebDriver after calling quit()?");
            }
        }

        HttpRequest httpRequest = this.commandCodec.encode(command);

        try {
            this.log("profiler", new HttpProfilerLogEntry(command.getName(), true));
            HttpResponse httpResponse = this.client.execute(httpRequest, true);
            this.log("profiler", new HttpProfilerLogEntry(command.getName(), false));
            Response response = this.responseCodec.decode(httpResponse);
            if (response.getSessionId() == null && httpResponse.getTargetHost() != null) {
                String sessionId = HttpSessionId.getSessionId(httpResponse.getTargetHost());
                response.setSessionId(sessionId);
            }

            if ("quit".equals(command.getName())) {
                this.client.close();
            }

            return response;
        } catch (UnsupportedCommandException var6) {
            if (var6.getMessage() != null && !"".equals(var6.getMessage())) {
                throw var6;
            } else {
                throw new UnsupportedOperationException("No information from server. Command name was: " + command.getName(), var6.getCause());
            }
        }
    }

  然后在org.openqa.selenium.remote.http.HttpRequest的encode方法封装http请求,封装完httpRequest后,后面就是转换相关的参数模拟浏览器访问发起请求了:

public HttpRequest encode(Command command) {
        JsonHttpCommandCodec.CommandSpec spec = (JsonHttpCommandCodec.CommandSpec)this.nameToSpec.get(command.getName());
        if (spec == null) {
            throw new UnsupportedCommandException(command.getName());
        } else {
            String uri = this.buildUri(command, spec);
            HttpRequest request = new HttpRequest(spec.method, uri);
            if (HttpMethod.POST == spec.method) {
                String content = this.beanToJsonConverter.convert(command.getParameters());
                byte[] data = content.getBytes(Charsets.UTF_8);
                request.setHeader("Content-Length", String.valueOf(data.length));
                request.setHeader("Content-Type", MediaType.JSON_UTF_8.toString());
                request.setContent(data);
            }

            if (HttpMethod.GET == spec.method) {
                request.setHeader("Cache-Control", "no-cache");
            }

            return request;
        }
    }

  整个过程中没有对传入的url进行相关的安全检查。底层实际上是通过org.apache.httpcomponents.httpclient来发起请求的。加上Java网络请求支持的协议,还可以使用file协议进行任意文件读取

wKg0C2Cx02AJAMhAACj0gPQ1Q058.png

  这里尝试对url参数传入file:///etc/passwd,成功截屏相关的文件内容:

wKg0C2CxGyAdmaDAABd2l8kifY058.png

四、其他

  除此之外,Java类库cdp4j也存在类似的问题(cdp4j具有清晰简洁的API,可自动执行基于Chrome / Chromium的浏览器。它使用Google Chrome DevTools协议来自动化基于Chrome / Chromium的浏览器。)

<dependency>
    <groupId>io.webfolder</groupId>
    <artifactId>cdp4j</artifactId>
    <version>2.2.1</version>
</dependency>

  例如如下例子:

        ArrayList<String> arguments= new ArrayList<String>();
        //如果添加此行就不会弹出google浏览器
        //arguments.add("--headless");
        Launcher launcher = new Launcher();
        //第一个参数是本地谷歌浏览器的可执行地址
        try (SessionFactory factory = launcher.launch(Arrays.asList("--disable-gpu", "--headless"));
             Session session = factory.create()) {
            //这个参数是你想要爬取的网址
            session.navigate("********");
            //等待加载完毕
            session.waitDocumentReady();
            //获得爬取的数据
            String content = (String) session.getProperty("//body", "outerText");
            System.out.println("---------");
            System.out.println(content);
        }

  如果navigate调用的url用户可控的话,那么存在ssrf风险,同样的也支持file协议:

wKg0C2Cxs36AEIQsAAA4FJil5QA151.png

  综上,在使用第三方jar进行相关的业务实现时,要结合实际的场景过滤/检查用户可控的参数内容。避免产生不必要的安全风险。同时在进行黑盒测试时,对于网页截图类的业务场景,也是需要覆盖测试的风险点。

免责声明:文章内容不代表本站立场,本站不对其内容的真实性、完整性、准确性给予任何担保、暗示和承诺,仅供读者参考,文章版权归原作者所有。如本文内容影响到您的合法权益(内容、图片等),请及时联系本站,我们会及时删除处理。查看原文

为您推荐