0x00漏洞描述:
ѕрееdtеѕt iѕ а vеrу liɡhtԝеiɡht nеtԝоrk ѕрееd tеѕtinɡ tооl imрlеmеntеd in Jаvаѕсriрt. Thеrе iѕ а Crоѕѕ-ѕitе Sсriрtinɡ vulnеrаbilitу in librеѕроndеd ѕрееdtеѕt 5.2.4 аnd еаrliеr vеrѕiоnѕ. Thе vulnеrаbilitу iѕ саuѕеd bу thе раrаmеtеr id оf thе filе rеѕultѕ/ѕtаtѕ.рhр thаt ԝill саuѕе Crоѕѕ-ѕitе Sсriрtinɡ.
0x01 分析:
先到Github把ѕрееdtеѕt的5.2.4版本的源代码下下来,然后直接看rеѕultѕ/ѕtаtѕ.рhр文件:
<?php
session_start();
error_reporting(0);
require 'telemetry_settings.php';
require_once 'telemetry_db.php';
header('Content-Type: text/html; charset=utf-8');
header('Cache-Control: no-store, no-cache, must-revalidate, max-age=0, s-maxage=0');
header('Cache-Control: post-check=0, pre-check=0', false);
header('Pragma: no-cache');
?>
<!DOCTYPE html>
<html>
<head>
<title>LibreSpeed - Stats</title>
<style type="text/css">
html,body{
margin:0;
padding:0;
border:none;
width:100%; min-height:100%;
}
html{
background-color: hsl(198,72%,35%);
font-family: "Segoe UI","Roboto",sans-serif;
}
body{
background-color:#FFFFFF;
box-sizing:border-box;
width:100%;
max-width:70em;
margin:4em auto;
box-shadow:0 1em 6em #00000080;
padding:1em 1em 4em 1em;
border-radius:0.4em;
}
h1,h2,h3,h4,h5,h6{
font-weight:300;
margin-bottom: 0.1em;
}
h1{
text-align:center;
}
table{
margin:2em 0;
width:100%;
}
table, tr, th, td {
border: 1px solid #AAAAAA;
}
th {
width: 6em;
}
td {
word-break: break-all;
}
div {
margin: 1em 0;
}
</style>
</head>
<body>
<h1>LibreSpeed - Stats</h1>
<?php
if (!isset($stats_password) || $stats_password === 'PASSWORD') {
?>
Please set $stats_password in telemetry_settings.php to enable access.
<?php
} elseif ($_SESSION['logged'] === true) {
if ($_GET['op'] === 'logout') {
$_SESSION['logged'] = false;
?><script type="text/javascript">window.location=location.protocol+"//"+location.host+location.pathname;</script><?php
} else {
?>
<form action="stats.php" method="GET"><input type="hidden" name="op" value="logout" /><input type="submit" value="Logout" /></form>
<form action="stats.php" method="GET">
<h3>Search test results</h3>
<input type="hidden" name="op" value="id" />
<input type="text" name="id" id="id" placeholder="Test ID" value=""/>
<input type="submit" value="Find" />
<input type="submit" onclick="document.getElementById('id').value=''" value="Show last 100 tests" />
</form>
<?php
if ($_GET['op'] === 'id' && !empty($_GET['id'])) {
$speedtest = getSpeedtestUserById($_GET['id']);
$speedtests = [];
if (false === $speedtest) {
echo '<div>There was an error trying to fetch the speedtest result for ID "'.$_GET['id'].'".</div>';
} elseif (null === $speedtest) {
echo '<div>Could not find a speedtest result for ID "'.$_GET['id'].'".</div>';
} else {
$speedtests = [$speedtest];
}
} else {
$speedtests = getLatestSpeedtestUsers();
if (false === $speedtests) {
echo '<div>There was an error trying to fetch latest speedtest results.</div>';
} elseif (empty($speedtests)) {
echo '<div>Could not find any speedtest results in database.</div>';
}
}
foreach ($speedtests as $speedtest) {
?>
<table>
<tr>
<th>Test ID</th>
<td><?= htmlspecialchars($speedtest['id_formatted'], ENT_HTML5, 'UTF-8') ?></td>
</tr>
<tr>
<th>Date and time</th>
<td><?= htmlspecialchars($speedtest['timestamp'], ENT_HTML5, 'UTF-8') ?></td>
</tr>
<tr>
<th>IP and ISP Info</th>
<td>
<?= htmlspecialchars($speedtest['ip'], ENT_HTML5, 'UTF-8') ?><br/>
<?= htmlspecialchars($speedtest['ispinfo'], ENT_HTML5, 'UTF-8') ?>
</td>
</tr>
<tr>
<th>User agent and locale</th>
<td><?= htmlspecialchars($speedtest['ua'], ENT_HTML5, 'UTF-8') ?><br/>
<?= htmlspecialchars($speedtest['lang'], ENT_HTML5, 'UTF-8') ?>
</td>
</tr>
<tr>
<th>Download speed</th>
<td><?= htmlspecialchars($speedtest['dl'], ENT_HTML5, 'UTF-8') ?></td>
</tr>
<tr>
<th>Upload speed</th>
<td><?= htmlspecialchars($speedtest['ul'], ENT_HTML5, 'UTF-8') ?></td>
</tr>
<tr>
<th>Ping</th>
<td><?= htmlspecialchars($speedtest['ping'], ENT_HTML5, 'UTF-8') ?></td>
</tr>
<tr>
<th>Jitter</th>
<td><?= htmlspecialchars($speedtest['jitter'], ENT_HTML5, 'UTF-8') ?></td>
</tr>
<tr>
<th>Log</th>
<td><?= htmlspecialchars($speedtest['log'], ENT_HTML5, 'UTF-8') ?></td>
</tr>
<tr>
<th>Extra info</th>
<td><?= htmlspecialchars($speedtest['extra'], ENT_HTML5, 'UTF-8') ?></td>
</tr>
</table>
<?php
}
}
} elseif ($_GET['op'] === 'login' && $_POST['password'] === $stats_password) {
$_SESSION['logged'] = true;
?><script type="text/javascript">window.location=location.protocol+"//"+location.host+location.pathname;</script><?php
} else {
?>
<form action="stats.php?op=login" method="POST">
<h3>Login</h3>
<input type="password" name="password" placeholder="Password" value=""/>
<input type="submit" value="Login" />
</form>
<?php
}
?>
</body>
</html>
直接看第89行的漏洞点:
<?php
if ($_GET['op'] === 'id' && !empty($_GET['id'])) {
$speedtest = getSpeedtestUserById($_GET['id']);
$speedtests = [];
if (false === $speedtest) {
echo '<div>There was an error trying to fetch the speedtest result for ID "'.$_GET['id'].'".</div>';
} elseif (null === $speedtest) {
echo '<div>Could not find a speedtest result for ID "'.$_GET['id'].'".</div>';
} else {
$speedtests = [$speedtest];
}
} else {
$speedtests = getLatestSpeedtestUsers();
if (false === $speedtests) {
echo '<div>There was an error trying to fetch latest speedtest results.</div>';
} elseif (empty($speedtests)) {
echo '<div>Could not find any speedtest results in database.</div>';
}
}
foreach ($speedtests as $speedtest) {
?>
当接收参数op=id,参数id=空或者flase时,直接将$_GET[‘id’]拼接到HTML代码中,导致了一个反射型的XSS。
0x02 复现
直接docker pull下来官方镜像,把里面的内容换成5.2.4版本的代码,然后点击登录:
这里由于stats.php的代码里有一句:
<?php
if (!isset($stats_password) || $stats_password === 'PASSWORD') {
?>
Please set $stats_password in telemetry_settings.php to enable access.
<?php
} elseif ($_SESSION['logged'] === true) {
if ($_GET['op'] === 'logout') {
$_SESSION['logged'] = false;
?><script type="text/javascript">window.location=location.protocol+"//"+location.host+location.pathname;</script><?php
因此,还要到telemetry_settings.php里去把$stats_password改掉。
登录进去之后输入payload:
利用成功:
0x03利用条件
1.知道stats.php的登录密码
2.目标给results文件夹配置了php执行的权限(实际上很多目标都没有给results文件夹php文件的解析权限)
3.反射型XSS,必须要与目标有交互
4.版本有极大限制,仅限于5.2.3和5.2.4版本,如5.2.2版本就不存在这个问题:
<?php
$q=null;
if($_GET["op"]=="id"&&!empty($_GET["id"])){
$id=$_GET["id"];
if($enable_id_obfuscation) $id=deobfuscateId($id);
if($db_type=="mysql"){
$q=$conn->prepare("select id,timestamp,ip,ispinfo,ua,lang,dl,ul,ping,jitter,log,extra from speedtest_users where id=?");
$q->bind_param("i",$id);
$q->execute();
$q->store_result();
$q->bind_result($id,$timestamp,$ip,$ispinfo,$ua,$lang,$dl,$ul,$ping,$jitter,$log,$extra);
} else if($db_type=="sqlite"||$db_type=="postgresql"){
$q=$conn->prepare("select id,timestamp,ip,ispinfo,ua,lang,dl,ul,ping,jitter,log,extra from speedtest_users where id=?");
$q->execute(array($id));
} else die();
}else{
if($db_type=="mysql"){
$q=$conn->prepare("select id,timestamp,ip,ispinfo,ua,lang,dl,ul,ping,jitter,log,extra from speedtest_users order by timestamp desc limit 0,100");
$q->execute();
$q->store_result();
$q->bind_result($id,$timestamp,$ip,$ispinfo,$ua,$lang,$dl,$ul,$ping,$jitter,$log,$extra);
} else if($db_type=="sqlite"||$db_type=="postgresql"){
$q=$conn->prepare("select id,timestamp,ip,ispinfo,ua,lang,dl,ul,ping,jitter,log,extra from speedtest_users order by timestamp desc limit 100");
$q->execute();
}else die();
}
while(true){
$id=null; $timestamp=null; $ip=null; $ispinfo=null; $ua=null; $lang=null; $dl=null; $ul=null; $ping=null; $jitter=null; $log=null; $extra=null;
if($db_type=="mysql"){
if(!$q->fetch()) break;
} else if($db_type=="sqlite"||$db_type=="postgresql"){
if(!($row=$q->fetch())) break;
$id=$row["id"];
$timestamp=$row["timestamp"];
$ip=$row["ip"];
$ispinfo=$row["ispinfo"];
$ua=$row["ua"];
$lang=$row["lang"];
$dl=$row["dl"];
$ul=$row["ul"];
$ping=$row["ping"];
$jitter=$row["jitter"];
$log=$row["log"];
$extra=$row["extra"];
}else die();
?>
<table>
<tr><th>Test ID</th><td><?=htmlspecialchars(($enable_id_obfuscation?(obfuscateId($id)." (deobfuscated: ".$id.")"):$id), ENT_HTML5, 'UTF-8') ?></td></tr>
<tr><th>Date and time</th><td><?=htmlspecialchars($timestamp, ENT_HTML5, 'UTF-8') ?></td></tr>
<tr><th>IP and ISP Info</th><td><?=$ip ?><br/><?=htmlspecialchars($ispinfo, ENT_HTML5, 'UTF-8') ?></td></tr>
<tr><th>User agent and locale</th><td><?=$ua ?><br/><?=htmlspecialchars($lang, ENT_HTML5, 'UTF-8') ?></td></tr>
<tr><th>Download speed</th><td><?=htmlspecialchars($dl, ENT_HTML5, 'UTF-8') ?></td></tr>
<tr><th>Upload speed</th><td><?=htmlspecialchars($ul, ENT_HTML5, 'UTF-8') ?></td></tr>
<tr><th>Ping</th><td><?=htmlspecialchars($ping, ENT_HTML5, 'UTF-8') ?></td></tr>
<tr><th>Jitter</th><td><?=htmlspecialchars($jitter, ENT_HTML5, 'UTF-8') ?></td></tr>
<tr><th>Log</th><td><?=htmlspecialchars($log, ENT_HTML5, 'UTF-8') ?></td></tr>
<tr><th>Extra info</th><td><?=htmlspecialchars($extra, ENT_HTML5, 'UTF-8') ?></td></tr>
</table>
<?php
}
?>
综合评价:利用条件极其苛刻,鸡肋漏洞。这种洞居然也是CVE,我真是服了。