JavaWeb网页截图中的ssrf

  • JavaWeb网页截图中的ssrf已关闭评论
  • 39 views
  • A+

一、相关背景

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

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

wKg0C2Cxtr2ABxIAAABZWtBY4E759.png

二、selenium+chrome headless

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

  相关依赖:

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

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

java
// 设置驱动地址
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方法:

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

  查看execute方法的具体实现:

```java
protected Response execute(String driverCommand, Map 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方法:

java
Command command = new Command(this.sessionId, driverCommand, parameters);

```java
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:

```java
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后,后面就是转换相关的参数模拟浏览器访问发起请求了:

```java
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的浏览器。)

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

  例如如下例子:

java
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进行相关的业务实现时,要结合实际的场景过滤/检查用户可控的参数内容。避免产生不必要的安全风险。同时在进行黑盒测试时,对于网页截图类的业务场景,也是需要覆盖测试的风险点。

相关推荐: 玩转(Windows)沙箱

备注 原文地址:Playing in the (Windows) Sandbox - Check Point Research 原文信息:March 11, 2021Research By: Alex Ilgayev 原文标题:Playing in the (…